diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..b46909e
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 DotKernel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..aa787ff
--- /dev/null
+++ b/README.md
@@ -0,0 +1,157 @@
+# pingu
+Application for Queue , using swoole and Lamins // Mezzio
+
+# Installing DotKernel `pingu`
+
+- [Installing DotKernel `pingu`](#installing-dotkernel-pingu)
+ - [Installation](#installation)
+ - [Composer](#composer)
+ - [Choose a destination path for DotKernel `pingu` installation](#choose-a-destination-path-for-dotkernel-pingu-installation)
+ - [Installing the `pingu` Composer package](#installing-the-pingu-composer-package)
+ - [Installing DotKernel pingu](#installing-dotkernel-pingu)
+ - [Configuration - First Run](#configuration---first-run)
+ - [Testing (Running)](#testing-running)
+
+### Composer
+
+Installation instructions:
+
+- [Composer Installation - Linux/Unix/OSX](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx)
+- [Composer Installation - Windows](https://getcomposer.org/doc/00-intro.md#installation-windows)
+
+> If you have never used composer before make sure you read the [`Composer Basic Usage`](https://getcomposer.org/doc/01-basic-usage.md) section in Composer's documentation
+
+## Choosing an installation path for DotKernel `pingu`
+
+Example:
+
+- absolute path `/var/www/pingu`
+- or relative path `pingu` (equivalent with `./pingu`)
+
+## Installing DotKernel `pingu`
+
+After choosing the path for DotKernel (`pingu` will be used for the remainder of this example) it must be installed.
+
+#### Installing DotKernel `pingu` using git clone
+
+This method requires more manual input, but it ensures that the default branch is installed, even if it is not released. Run the following command:
+
+```bash
+$ git clone https://github.com/dotkernel/pingu.git .
+```
+
+The dependencies have to be installed separately, by running this command
+```bash
+$ composer install
+```
+
+The setup asks for configuration settings regarding injections (type `0` and hit `enter`) and a confirmation to use this setting for other packages (type `y` and hit `enter`)
+
+## Configuration - First Run
+
+- Remove the `.dist` extension from the files `config/autoload/local.php.dist`, `config/autoload/mail.local.php.dist`, `config/autoload/notification.local.php.dist`, `config/autoload/redis.local.php.dist`, `config/autoload/swoole.local.php.dist`
+- Edit `config/autoload/local.php` according to your dev machine and fill in the `database` configuration
+- Edit `config/autoload/notification.php` by filling the 'protocol' and 'host' configuration
+- Add smtp credentials in `config/autoload/mail.local.php` if you want your application to send mails on registration etc.
+- Create `data/logs` folder and leave it empty
+
+> Charset recommendation: utf8mb4_general_ci
+
+## Testing (Running)
+
+Note: **Do not enable dev mode in production**
+
+- Run the following commands in your project's directory ( in different tabs ):
+
+```bash
+$ redis-cli
+$ php bin/dot-swoole start
+$ vendor/bin/qjitsu work
+$ vendor/bin/qjitsu scheduler:run --interval=1
+```
+
+> Tip: use --interval=1 on dev only
+
+**NOTE:**
+If you are still getting exceptions or errors regarding some missing services, try running the following command
+
+```bash
+$ php bin/clear-config-cache.php
+```
+
+> If `config-cache.php` is present that config will be loaded regardless of the `ConfigAggregator::ENABLE_CACHE` in `config/autoload/mezzio.global.php`
+
+## Daemons (services) files content
+```bash
+app-main.service
+[Unit]
+Description=pingu startup service
+StartLimitIntervalSec=1
+
+[Service]
+#The dummy program will exit
+Type=oneshot
+ExecStart=/bin/true
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
+```
+
+```bash
+app-queue.service
+[Unit]
+Description=Queue startup service
+After=mysqld.service
+PartOf=app-main.service
+StartLimitIntervalSec=1
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+User=root
+ExecStart=/usr/bin/php /var/www/html/app-directory/vendor/bin/qjitsu work
+
+[Install]
+WantedBy=app-main.service
+```
+
+```bash
+app-queue-scheduler.service
+[Unit]
+Description=Queue scheduler startup service
+After=mysqld.service
+PartOf=app-main.service
+StartLimitIntervalSec=1
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+User=root
+ExecStart=/usr/bin/php /var/www/html/app-directory/vendor/bin/qjitsu scheduler:run --interval=1
+
+[Install]
+WantedBy=app-main.service
+```
+
+```bash
+app-swoole.service
+[Unit]
+Description=pingu startup service
+After=mysqld.service
+PartOf=app-main.service
+StartLimitIntervalSec=1
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+User=root
+ExecStart=/usr/bin/php /var/www/html/app-directory/bin/dot-swoole start
+ExecStop=/usr/bin/php /var/www/html/app-directory/bin/dot-swoole stop
+
+[Install]
+WantedBy=app-main.service
+```
diff --git a/bin/clear-config-cache.php b/bin/clear-config-cache.php
new file mode 100644
index 0000000..2fcf36b
--- /dev/null
+++ b/bin/clear-config-cache.php
@@ -0,0 +1,48 @@
+setAutoExit(true);
+$commandLine->setCommandLoader(new ContainerCommandLoader($container, [
+ 'reload' => ReloadCommand::class,
+ 'start' => StartCommand::class,
+ 'status' => StatusCommand::class,
+ 'stop' => StopCommand::class,
+]));
+
+$commandLine->run();
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..9be0993
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,115 @@
+{
+ "name": "dotkernel/pingu",
+ "description": "Dotkernel Notification Application based on Swoole",
+ "type": "project",
+ "license": "MIT",
+ "homepage": "https://github.com/dotkernel/pingu",
+ "authors": [
+ {
+ "name": "DotKernel Team",
+ "email": "team@dotkernel.com"
+ }
+ ],
+ "config": {
+ "sort-packages": true
+ },
+ "extra": {
+ "zf": {
+ "component-whitelist": [
+ "mezzio/mezzio",
+ "mezzio/mezzio-helpers",
+ "mezzio/mezzio-router",
+ "laminas/laminas-httphandlerrunner",
+ "mezzio/mezzio-laminasrouter"
+ ]
+ }
+ },
+ "support": {
+ "issues": "https://github.com/mezzio/mezzio-skeleton/issues",
+ "source": "https://github.com/mezzio/mezzio-skeleton",
+ "rss": "https://github.com/mezzio/mezzio-skeleton/releases.atom",
+ "slack": "https://zendframework-slack.herokuapp.com",
+ "forum": "https://discourse.zendframework.com/c/questions/expressive"
+ },
+ "require": {
+ "php": "^7.4",
+ "ext-json": "*",
+ "ext-swoole": "*",
+ "dasprid/container-interop-doctrine": "^1.1",
+ "dotkernel/dot-annotated-services": "^3.0.2",
+ "laminas/laminas-cache": "^2.9",
+ "laminas/laminas-component-installer": "^2.1.2",
+ "laminas/laminas-config-aggregator": "^1.2.2",
+ "laminas/laminas-dependency-plugin": "^1.0.3",
+ "laminas/laminas-diactoros": "^1.7.1 || ^2.0",
+ "laminas/laminas-log": "^2.12",
+ "laminas/laminas-mail": "^2.10.1",
+ "laminas/laminas-servicemanager": "^3.4",
+ "laminas/laminas-stdlib": "^3.2.1",
+ "mez/queuejitsu": "^3.0.1",
+ "mez/queuejitsu-cli": "^0.1.6",
+ "mez/queuejitsu-scheduler": "^0.2.2",
+ "mez/queuejitsu-scheduler-cli": "^0.2.2",
+ "mezzio/mezzio": "^3.2.2",
+ "mezzio/mezzio-fastroute": "^3.0.3",
+ "mezzio/mezzio-helpers": "^5.3",
+ "mezzio/mezzio-laminasrouter": "^3.0.1",
+ "mezzio/mezzio-twigrenderer": "^2.6.1",
+ "predis/predis": "^1.1.1",
+ "ramsey/uuid-doctrine": "^1.6"
+ },
+ "require-dev": {
+ "filp/whoops": "^2.7.2",
+ "phpunit/phpunit": "^9.1.4",
+ "roave/security-advisories": "dev-master",
+ "squizlabs/php_codesniffer": "^3.5.5",
+ "symfony/messenger": "^5.0.8",
+ "mezzio/mezzio-tooling": "^1.3.0",
+ "laminas/laminas-development-mode": "^3.2"
+ },
+ "autoload": {
+ "psr-4": {
+ "App\\": "src/App/src/",
+ "Notification\\": "src/Notification/",
+ "Dot\\Swoole\\": "src/Swoole/",
+ "Core\\": "src/Core/",
+ "Core\\User\\": "src/Core/src/User/",
+ "Core\\Common\\": "src/Core/src/Common",
+ "Core\\Workspace\\": "src/Core/src/Workspace/",
+ "Core\\Team\\": "src/Core/src/Team/",
+ "Core\\Poll\\": "src/Core/src/Poll/",
+ "Core\\Question\\": "src/Core/src/Question/",
+ "Core\\Skill\\": "src/Core/src/Skill/",
+ "Core\\Notification\\": "src/Core/src/Notification/",
+ "Core\\NotificationSystem\\": "src/Core/src/NotificationSystem/",
+ "Core\\Task\\": "src/Core/src/Task/",
+ "Core\\Doctrine\\": "src/Core/src/Doctrine/",
+ "Core\\Report\\": "src/Core/src/Report/",
+ "Core\\Image\\": "src/Core/src/Image/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "AppTest\\": "test/AppTest/"
+ }
+ },
+ "scripts": {
+ "post-create-project-cmd": [
+ "@development-enable"
+ ],
+ "development-disable": "laminas-development-mode disable",
+ "development-enable": "laminas-development-mode enable",
+ "development-status": "laminas-development-mode status",
+ "mezzio": "mezzio --ansi",
+ "check": [
+ "@cs-check",
+ "@test"
+ ],
+ "clear-config-cache": "php bin/clear-config-cache.php",
+ "cs-check": "phpcs",
+ "cs-fix": "phpcbf",
+ "serve": "php -S 0.0.0.0:8080 -t public/",
+ "test": "phpunit --colors=always",
+ "test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
+ }
+}
diff --git a/config/.gitignore b/config/.gitignore
new file mode 100644
index 0000000..9604301
--- /dev/null
+++ b/config/.gitignore
@@ -0,0 +1 @@
+development.config.php
diff --git a/config/autoload/.gitignore b/config/autoload/.gitignore
new file mode 100644
index 0000000..1a83fda
--- /dev/null
+++ b/config/autoload/.gitignore
@@ -0,0 +1,2 @@
+local.php
+*.local.php
diff --git a/config/autoload/dependencies.global.php b/config/autoload/dependencies.global.php
new file mode 100644
index 0000000..a9ab249
--- /dev/null
+++ b/config/autoload/dependencies.global.php
@@ -0,0 +1,26 @@
+ [
+ // Use 'aliases' to alias a service name to another service. The
+ // key is the alias name, the value is the service to which it points.
+ 'aliases' => [
+ // Fully\Qualified\ClassOrInterfaceName::class => Fully\Qualified\ClassName::class,
+ ],
+ // Use 'invokables' for constructor-less services, or services that do
+ // not require arguments to the constructor. Map a service name to the
+ // class name.
+ 'invokables' => [
+ // Fully\Qualified\InterfaceName::class => Fully\Qualified\ClassName::class,
+ ],
+ // Use 'factories' for services provided by callbacks/factory classes.
+ 'factories' => [
+ // Fully\Qualified\ClassName::class => Fully\Qualified\FactoryName::class,
+ ],
+ ],
+];
diff --git a/config/autoload/development.local.php.dist b/config/autoload/development.local.php.dist
new file mode 100644
index 0000000..587fb07
--- /dev/null
+++ b/config/autoload/development.local.php.dist
@@ -0,0 +1,39 @@
+ [
+ 'invokables' => [
+ ],
+ 'factories' => [
+ ErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class,
+ 'Mezzio\Whoops' => Container\WhoopsFactory::class,
+ 'Mezzio\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class,
+ ],
+ ],
+
+ 'whoops' => [
+ 'json_exceptions' => [
+ 'display' => true,
+ 'show_trace' => true,
+ 'ajax_only' => true,
+ ],
+ ],
+ 'twig' => [
+ 'debug' => true,
+ 'cache_dir' => false,
+ ]
+];
diff --git a/config/autoload/doctrine.global.php b/config/autoload/doctrine.global.php
new file mode 100644
index 0000000..b7468bc
--- /dev/null
+++ b/config/autoload/doctrine.global.php
@@ -0,0 +1,47 @@
+ [
+ 'factories' => [
+ 'doctrine.entity_manager.orm_default' => \ContainerInteropDoctrine\EntityManagerFactory::class,
+ ],
+ 'aliases' => [
+ \Doctrine\ORM\EntityManager::class => 'doctrine.entity_manager.orm_default',
+ \Doctrine\ORM\EntityManagerInterface::class => 'doctrine.entity_manager.default',
+ 'doctrine.entitymanager.orm_default' => 'doctrine.entity_manager.orm_default'
+ ]
+ ],
+
+ 'doctrine' => [
+ 'connection' => [
+ 'orm_default' => [
+ 'doctrine_mapping_types' => [
+ UuidBinaryType::NAME => 'binary',
+ UuidBinaryOrderedTimeType::NAME => 'binary',
+ ]
+ ]
+ ],
+ 'configuration' => [
+ 'orm_default' => [
+ 'proxy_dir' => __DIR__ . '/../../data/cache/DoctrineEntityProxy',
+ ],
+ ],
+ 'driver' => [
+ // default metadata driver, aggregates all other drivers into a single one.
+ // Override `orm_default` only if you know what you're doing
+ 'orm_default' => [
+ 'class' => \Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain::class,
+ 'drivers' => [],
+ ],
+ ],
+ 'types' => [
+ UuidType::NAME => UuidType::class,
+ UuidBinaryType::NAME => UuidBinaryType::class,
+ UuidBinaryOrderedTimeType::NAME => UuidBinaryOrderedTimeType::class,
+ ],
+ ],
+];
diff --git a/config/autoload/local.php.dist b/config/autoload/local.php.dist
new file mode 100644
index 0000000..d357363
--- /dev/null
+++ b/config/autoload/local.php.dist
@@ -0,0 +1,66 @@
+ '',
+ 'database' => '',
+ 'username' => '',
+ 'password' => '',
+ 'port' => 3306,
+ 'driver' => 'pdo_mysql',
+ 'charset' => 'utf8mb4',
+ 'collate' => 'utf8mb4_general_ci',
+ //add more database connection params
+];
+
+return [
+ 'database' => $database,
+
+ 'doctrine' => [
+ 'connection' => [
+ 'orm_default' => [
+ 'driverClass' => \Doctrine\DBAL\Driver\PDOMySql\Driver::class,
+ 'params' => [
+ 'host' => $database['hostname'],
+ 'port' => $database['port'],
+ 'user' => $database['username'],
+ 'password' => $database['password'],
+ 'dbname' => $database['database'],
+ 'charset' => $database['charset'],
+ 'collate' => $database['collate'],
+ ],
+ ],
+ ],
+ ],
+
+ 'cors' => [
+ 'origin' => ['*'],
+ 'methods' => ['DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT'],
+ 'headers.allow' => ['Accept', 'Content-Type', 'Authorization'],
+ 'headers.expose' => [],
+ 'credentials' => false,
+ 'cache' => 0,
+ 'error' => [
+ \App\Cors\Factory\CorsFactory::class, 'error'
+ ]
+ ],
+
+ 'connections' => [
+ 'redis' => [
+ 'driver' => 'redis',
+ 'connection' => 'default',
+ 'queue' => 'raw',
+ 'retry_after' => 90,
+ 'block_for' => null,
+ ],
+ ],
+
+ 'api_url' => 'http://localhost:8081',
+];
diff --git a/config/autoload/log.global.php b/config/autoload/log.global.php
new file mode 100644
index 0000000..3a77426
--- /dev/null
+++ b/config/autoload/log.global.php
@@ -0,0 +1,32 @@
+ [
+ 'queue_log' => [
+ 'writers' => [
+ 'FileWriter' => [
+ 'name' => 'stream',
+ 'priority' => \Laminas\Log\Logger::NOTICE,
+ 'options' => [
+ 'stream' => sprintf('%s/../../data/logs/error-log-queue.log',
+ __DIR__
+ ),
+ // explicitly log all messages
+ 'filters' => [
+ 'allMessages' => [
+ 'name' => 'priority',
+ 'options' => [
+ 'operator' => '<=',
+ 'priority' => \Laminas\Log\Logger::NOTICE,
+ ],
+ ],
+ ],
+ 'formatter' => [
+ 'name' => \Laminas\Log\Formatter\Json::class,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+];
diff --git a/config/autoload/mail.local.php.dist b/config/autoload/mail.local.php.dist
new file mode 100644
index 0000000..71b52b3
--- /dev/null
+++ b/config/autoload/mail.local.php.dist
@@ -0,0 +1,20 @@
+ [
+ 'name' => 'smtp.gmail.com',
+ 'host' => 'smtp.gmail.com',
+ 'port' => 587,
+ 'connection_class' => 'login',
+ 'connection_config' => [
+ 'username' => '****@***.com',
+ 'password' => '*****',
+ 'host'=>'localhost:8556',
+ 'ssl' => 'tls',
+ ]
+ ],
+ 'application' => [
+ 'name' => 'My app',
+ 'email' => 'app@eamil.com'
+ ]
+];
\ No newline at end of file
diff --git a/config/autoload/mezzio.global.php b/config/autoload/mezzio.global.php
new file mode 100644
index 0000000..e1b70e5
--- /dev/null
+++ b/config/autoload/mezzio.global.php
@@ -0,0 +1,25 @@
+ true,
+
+ // Enable debugging; typically used to provide debugging information within templates.
+ 'debug' => false,
+
+ 'mezzio' => [
+ // Provide templates for the error handling middleware to use when
+ // generating responses.
+ 'error_handler' => [
+ 'template_404' => 'error::404',
+ 'template_error' => 'error::error',
+ ],
+ ],
+];
diff --git a/config/autoload/notification.local.php.dist b/config/autoload/notification.local.php.dist
new file mode 100644
index 0000000..5ac4261
--- /dev/null
+++ b/config/autoload/notification.local.php.dist
@@ -0,0 +1,8 @@
+ [
+ 'protocol' => 'http', // https or http
+ 'host' => 'app.com' //add port if needed ex : app.com:8080
+ ]
+];
diff --git a/config/autoload/redis.local.php.dist b/config/autoload/redis.local.php.dist
new file mode 100644
index 0000000..cc9fd70
--- /dev/null
+++ b/config/autoload/redis.local.php.dist
@@ -0,0 +1,15 @@
+ [
+ 'cli' => [
+ 'store' => [
+ 'redis' => [
+ 'scheme' => 'tcp',
+ 'host' => 'localhost',
+ 'port' => 6379,
+ ]
+ ]
+ ]
+ ]
+];
diff --git a/config/autoload/swoole.local.php.dist b/config/autoload/swoole.local.php.dist
new file mode 100644
index 0000000..7411b19
--- /dev/null
+++ b/config/autoload/swoole.local.php.dist
@@ -0,0 +1,37 @@
+ [
+ // Available in Swoole 4.1 and up; enables coroutine support
+ // for most I/O operations:
+ 'enable_coroutine' => true,
+
+ 'swoole-server' => [
+ 'host' => 'localhost',
+ 'port' => 8556,
+ 'mode' => SWOOLE_PROCESS, // SWOOLE_BASE or SWOOLE_PROCESS;
+ 'protocol' => SWOOLE_SOCK_TCP, //| SWOOLE_SSL, // SSL-enable the server
+ 'options' => [
+ 'task_worker_num' => 3, // The number of Task Workers
+
+ // Set the SSL certificate and key paths for SSL support:
+ 'ssl_cert_file' => 'path/to/ssl.crt',
+ 'ssl_key_file' => 'path/to/ssl.key',
+
+ 'package_eof' => "\n",
+ 'open_eof_check' => true,
+ 'open_length_check' => true,
+
+ // Overwrite the default location of the pid file;
+ // required when you want to run multiple instances of your service in different ports:
+ 'pid_file' => '/tmp/pingu.pid',
+ ],
+ // Since 2.1.0: Set the process name prefix.
+ // The master process will be named `{prefix}-master`,
+ // worker processes will be named `{prefix}-worker-{id}`,
+ // and task worker processes will be named `{prefix}-task-worker-{id}`
+ 'process-name' => 'pingu',
+ ],
+ ],
+];
+
diff --git a/config/autoload/templates.global.php b/config/autoload/templates.global.php
new file mode 100644
index 0000000..115a335
--- /dev/null
+++ b/config/autoload/templates.global.php
@@ -0,0 +1,35 @@
+ [
+ 'factories' => [
+ Twig\Environment::class => TwigEnvironmentFactory::class,
+ TemplateRendererInterface::class => TwigRendererFactory::class,
+ ],
+ ],
+
+ 'templates' => [
+ 'extension' => 'html.twig',
+ ],
+
+ 'twig' => [
+ 'cache_dir' => __DIR__ . '/../../data/cache/twig',
+ 'assets_url' => '/',
+ 'assets_version' => 1,
+ 'extensions' => [
+ // extension service names or instances
+ ],
+ 'runtime_loaders' => [
+ // runtime loader names or instances
+
+ ],
+ 'globals' => [
+ // Variables to pass to all twig templates
+ ],
+ // 'timezone' => 'default timezone identifier; e.g. America/Chicago',
+ ],
+];
diff --git a/config/config.php b/config/config.php
new file mode 100644
index 0000000..ee4cb01
--- /dev/null
+++ b/config/config.php
@@ -0,0 +1,52 @@
+ __DIR__ . '/../data/cache/config-cache.php',
+];
+
+$aggregator = new ConfigAggregator([
+ \Laminas\HttpHandlerRunner\ConfigProvider::class,
+ \Mezzio\Router\FastRouteRouter\ConfigProvider::class,
+ // Include cache configuration
+ new ArrayProvider($cacheConfig),
+
+ \Mezzio\Helper\ConfigProvider::class,
+ \Mezzio\ConfigProvider::class,
+ \Mezzio\Router\ConfigProvider::class,
+ \Laminas\Mail\ConfigProvider::class,
+ \Mezzio\Twig\ConfigProvider::class,
+
+ // Log module config
+ Laminas\Log\ConfigProvider::class,
+
+ // Default App module config
+ App\ConfigProvider::class,
+ Notification\ConfigProvider::class,
+ \Dot\Swoole\ConfigProvider::class,
+ \QueueJitsu\ConfigProvider::class,
+ \QueueJitsu\Scheduler\ConfigProvider::class,
+ \QueueJitsu\Cli\ConfigProvider::class,
+ \QueueJitsu\Scheduler\Cli\ConfigProvider::class,
+
+
+ // Load application config in a pre-defined order in such a way that local settings
+ // overwrite global settings. (Loaded as first to last):
+ // - `global.php`
+ // - `*.global.php`
+ // - `local.php`
+ // - `*.local.php`
+ new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'),
+
+ // Load development config if it exists
+ new PhpFileProvider(realpath(__DIR__) . '/development.config.php'),
+], $cacheConfig['config_cache_path'], [\Laminas\ZendFrameworkBridge\ConfigPostProcessor::class]);
+
+return $aggregator->getMergedConfig();
diff --git a/config/container.php b/config/container.php
new file mode 100644
index 0000000..5109c1b
--- /dev/null
+++ b/config/container.php
@@ -0,0 +1,14 @@
+ true,
+ ConfigAggregator::ENABLE_CACHE => false,
+];
diff --git a/config/pipeline.php b/config/pipeline.php
new file mode 100644
index 0000000..e27e04a
--- /dev/null
+++ b/config/pipeline.php
@@ -0,0 +1,78 @@
+pipe(ErrorHandler::class);
+ $app->pipe(ServerUrlMiddleware::class);
+
+ // Pipe more middleware here that you want to execute on every request:
+ // - bootstrapping
+ // - pre-conditions
+ // - modifications to outgoing responses
+ //
+ // Piped Middleware may be either callables or service names. Middleware may
+ // also be passed as an array; each item in the array must resolve to
+ // middleware eventually (i.e., callable or service name).
+ //
+ // Middleware can be attached to specific paths, allowing you to mix and match
+ // applications under a common domain. The handlers in each middleware
+ // attached this way will see a URI with the matched path segment removed.
+ //
+ // i.e., path of "/api/member/profile" only passes "/member/profile" to $apiMiddleware
+ // - $app->pipe('/api', $apiMiddleware);
+ // - $app->pipe('/docs', $apiDocMiddleware);
+ // - $app->pipe('/files', $filesMiddleware);
+
+ // Register the routing middleware in the middleware pipeline.
+ // This middleware registers the Mezzio\Router\RouteResult request attribute.
+ $app->pipe(RouteMiddleware::class);
+
+ // The following handle routing failures for common conditions:
+ // - HEAD request but no routes answer that method
+ // - OPTIONS request but no routes answer that method
+ // - method not allowed
+ // Order here matters; the MethodNotAllowedMiddleware should be placed
+ // after the Implicit*Middleware.
+ $app->pipe(ImplicitHeadMiddleware::class);
+ $app->pipe(ImplicitOptionsMiddleware::class);
+ $app->pipe(MethodNotAllowedMiddleware::class);
+
+ // Seed the UrlHelper with the routing results:
+ $app->pipe(UrlHelperMiddleware::class);
+
+ $app->pipe(DefaultTemplateParamsMiddleware::class);
+ // Add more middleware here that needs to introspect the routing results; this
+ // might include:
+ //
+ // - route-based authentication
+ // - route-based validation
+ // - etc.
+
+ // Register the dispatch middleware in the middleware pipeline
+ $app->pipe(DispatchMiddleware::class);
+
+ // At this point, if no Response is returned by any middleware, the
+ // NotFoundHandler kicks in; alternately, you can provide other fallback
+ // middleware to execute.
+ $app->pipe(NotFoundHandler::class);
+};
diff --git a/config/routes.php b/config/routes.php
new file mode 100644
index 0000000..1a2a623
--- /dev/null
+++ b/config/routes.php
@@ -0,0 +1,50 @@
+get('/', App\Handler\HomePageHandler::class, 'home');
+ * $app->post('/album', App\Handler\AlbumCreateHandler::class, 'album.create');
+ * $app->put('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.put');
+ * $app->patch('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.patch');
+ * $app->delete('/album/:id', App\Handler\AlbumDeleteHandler::class, 'album.delete');
+ *
+ * Or with multiple request methods:
+ *
+ * $app->route('/contact', App\Handler\ContactHandler::class, ['GET', 'POST', ...], 'contact');
+ *
+ * Or handling all request methods:
+ *
+ * $app->route('/contact', App\Handler\ContactHandler::class)->setName('contact');
+ *
+ * or:
+ *
+ * $app->route(
+ * '/contact',
+ * App\Handler\ContactHandler::class,
+ * Mezzio\Router\Route::HTTP_METHOD_ANY,
+ * 'contact'
+ * );
+ */
+return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void {
+ $app->get('/', App\Handler\HomePageHandler::class, 'home');
+ $app->route(
+ '/notification',
+ Notification\Handler\NotificationHandler::class,
+ [
+ RequestMethod::METHOD_DELETE,
+ RequestMethod::METHOD_GET,
+ RequestMethod::METHOD_POST,
+ RequestMethod::METHOD_PATCH
+ ],
+ 'notification'
+ );
+ $app->get('/api/ping', App\Handler\PingHandler::class, 'api.ping');
+};
diff --git a/data/.gitignore b/data/.gitignore
new file mode 100644
index 0000000..5381e79
--- /dev/null
+++ b/data/.gitignore
@@ -0,0 +1,4 @@
+*
+!cache
+!cache/.gitkeep
+!.gitignore
diff --git a/data/cache/.gitkeep b/data/cache/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..de3035d
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,20 @@
+
+
+ Expressive Skeleton coding standard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ src
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..e9e72c0
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,17 @@
+
+
+
+
+ ./test
+
+
+
+
+
+ ./src
+
+
+
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..cd6355d
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,19 @@
+RewriteEngine On
+# The following rule allows authentication to work with fast-cgi
+RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+# The following rule tells Apache that if the requested filename
+# exists, simply serve it.
+RewriteCond %{REQUEST_FILENAME} -s [OR]
+RewriteCond %{REQUEST_FILENAME} -l [OR]
+RewriteCond %{REQUEST_FILENAME} -d
+RewriteRule ^.*$ - [NC,L]
+
+# The following rewrites all other queries to index.php. The
+# condition ensures that if you are using Apache aliases to do
+# mass virtual hosting, the base path will be prepended to
+# allow proper resolution of the index.php file; it will work
+# in non-aliased environments as well, providing a safe, one-size
+# fits all solution.
+RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
+RewriteRule ^(.*) - [E=BASE:%1]
+RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L]
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..f5ec42a
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,30 @@
+get(\Mezzio\Application::class);
+ $factory = $container->get(\Mezzio\MiddlewareFactory::class);
+
+ // Execute programmatic/declarative middleware pipeline and routing
+ // configuration statements
+ (require 'config/pipeline.php')($app, $factory, $container);
+ (require 'config/routes.php')($app, $factory, $container);
+
+ $app->run();
+})();
diff --git a/src/App/src/ConfigProvider.php b/src/App/src/ConfigProvider.php
new file mode 100644
index 0000000..dab14bb
--- /dev/null
+++ b/src/App/src/ConfigProvider.php
@@ -0,0 +1,57 @@
+ $this->getDependencies(),
+// 'templates' => $this->getTemplates(),
+ ];
+ }
+
+ /**
+ * Returns the container dependencies
+ */
+ public function getDependencies() : array
+ {
+ return [
+ 'invokables' => [
+ Handler\PingHandler::class => Handler\PingHandler::class,
+ ],
+ 'factories' => [
+ Handler\HomePageHandler::class => Handler\HomePageHandlerFactory::class,
+ ],
+ ];
+ }
+
+ /**
+ * Returns the templates configuration
+ */
+ public function getTemplates() : array
+ {
+ return [
+ 'paths' => [
+ 'app' => [__DIR__ . '/../templates/app'],
+ 'error' => [__DIR__ . '/../templates/error'],
+ 'layout' => [__DIR__ . '/../templates/layout'],
+ ],
+ ];
+ }
+}
diff --git a/src/App/src/Handler/HomePageHandler.php b/src/App/src/Handler/HomePageHandler.php
new file mode 100644
index 0000000..8480942
--- /dev/null
+++ b/src/App/src/Handler/HomePageHandler.php
@@ -0,0 +1,102 @@
+containerName = $containerName;
+ $this->router = $router;
+ $this->template = $template;
+ }
+
+ public function handle(ServerRequestInterface $request) : ResponseInterface
+ {
+// if ($this->template === null) {
+ return new JsonResponse([
+ 'welcome' => 'Congratulations! You have installed the zend-expressive skeleton application.',
+ 'docsUrl' => 'https://docs.zendframework.com/zend-expressive/',
+ ]);
+// }
+
+// $data = [];
+//
+// switch ($this->containerName) {
+// case 'Aura\Di\Container':
+// $data['containerName'] = 'Aura.Di';
+// $data['containerDocs'] = 'http://auraphp.com/packages/2.x/Di.html';
+// break;
+// case 'Pimple\Container':
+// $data['containerName'] = 'Pimple';
+// $data['containerDocs'] = 'https://pimple.symfony.com/';
+// break;
+// case 'Laminas\ServiceManager\ServiceManager':
+// $data['containerName'] = 'Zend Servicemanager';
+// $data['containerDocs'] = 'https://docs.zendframework.com/zend-servicemanager/';
+// break;
+// case 'Auryn\Injector':
+// $data['containerName'] = 'Auryn';
+// $data['containerDocs'] = 'https://github.com/rdlowrey/Auryn';
+// break;
+// case 'Symfony\Component\DependencyInjection\ContainerBuilder':
+// $data['containerName'] = 'Symfony DI Container';
+// $data['containerDocs'] = 'https://symfony.com/doc/current/service_container.html';
+// break;
+// case 'Zend\DI\Config\ContainerWrapper':
+// case 'DI\Container':
+// $data['containerName'] = 'PHP-DI';
+// $data['containerDocs'] = 'http://php-di.org';
+// break;
+// }
+//
+// if ($this->router instanceof Router\AuraRouter) {
+// $data['routerName'] = 'Aura.Router';
+// $data['routerDocs'] = 'http://auraphp.com/packages/2.x/Router.html';
+// } elseif ($this->router instanceof Router\FastRouteRouter) {
+// $data['routerName'] = 'FastRoute';
+// $data['routerDocs'] = 'https://github.com/nikic/FastRoute';
+// } elseif ($this->router instanceof Router\LaminasRouter) {
+// $data['routerName'] = 'Zend Router';
+// $data['routerDocs'] = 'https://docs.zendframework.com/zend-router/';
+// }
+//
+// if ($this->template instanceof PlatesRenderer) {
+// $data['templateName'] = 'Plates';
+// $data['templateDocs'] = 'http://platesphp.com/';
+// } elseif ($this->template instanceof TwigRenderer) {
+// $data['templateName'] = 'Twig';
+// $data['templateDocs'] = 'http://twig.sensiolabs.org/documentation';
+// } elseif ($this->template instanceof LaminasViewRenderer) {
+// $data['templateName'] = 'Zend View';
+// $data['templateDocs'] = 'https://docs.zendframework.com/zend-view/';
+// }
+//
+// return new HtmlResponse($this->template->render('app::home-page', $data));
+ }
+}
diff --git a/src/App/src/Handler/HomePageHandlerFactory.php b/src/App/src/Handler/HomePageHandlerFactory.php
new file mode 100644
index 0000000..7e27759
--- /dev/null
+++ b/src/App/src/Handler/HomePageHandlerFactory.php
@@ -0,0 +1,25 @@
+get(RouterInterface::class);
+ $template = $container->has(TemplateRendererInterface::class)
+ ? $container->get(TemplateRendererInterface::class)
+ : null;
+
+ return new HomePageHandler(get_class($container), $router, $template);
+ }
+}
diff --git a/src/App/src/Handler/PingHandler.php b/src/App/src/Handler/PingHandler.php
new file mode 100644
index 0000000..52c3ef1
--- /dev/null
+++ b/src/App/src/Handler/PingHandler.php
@@ -0,0 +1,20 @@
+ time()]);
+ }
+}
diff --git a/src/Notification/Adapter/RedisAdapter.php b/src/Notification/Adapter/RedisAdapter.php
new file mode 100644
index 0000000..a802f9e
--- /dev/null
+++ b/src/Notification/Adapter/RedisAdapter.php
@@ -0,0 +1,287 @@
+client = $client;
+ }
+
+ /**
+ * enqueueAt
+ *
+ * @param int $at
+ * @param Job $job
+ */
+ public function enqueueAt(int $at, Job $job): void
+ {
+ $data = $job->getPayload();
+
+ $data['s_time'] = time();
+
+ $key = sprintf('%s:%s', self::AT_QUEUE_NAME, $at);
+ $this->client->rpush($key, [json_encode($data)]);
+ $this->client->zadd(self::AT_QUEUE_NAME, [$at => $at]);
+ }
+
+ /**
+ * enqueueCron
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ *
+ * @param string $cron
+ * @param Job $job
+ *
+ * @throws \RuntimeException
+ */
+ public function enqueueCron(string $cron, Job $job): void
+ {
+ $data = ['cron' => $cron, 'job' => $job->getPayload()];
+
+ $id = Uuid::uuid4()->toString();
+
+ $key = sprintf('%s:%s', self::CRON_QUEUE_NAME, $id);
+
+ $this->client->set($key, json_encode($data));
+
+ $this->updateCron($id, $cron);
+ }
+
+ /**
+ * getNextJob
+ *
+ * @throws \RuntimeException
+ *
+ * @return null|Job
+ */
+ public function getNextJob(): ?Job
+ {
+ if ($this->hasAtJobsToProcess() && $this->hasCronJobsToProcess()) {
+ return $this->findNextJob();
+ }
+
+ if ($this->hasAtJobsToProcess()) {
+ return $this->getNextAtJob();
+ }
+
+ return $this->getNextCronJob();
+ }
+
+ /**
+ * getNextAtTimestamp
+ *
+ * @return int|null
+ */
+ protected function getNextAtTimestamp(): ?int
+ {
+ $at = time();
+
+ $items = $this->client->zrangebyscore(self::AT_QUEUE_NAME, '-inf', $at, ['limit', 0, 1]);
+ if (empty($items)) {
+ return null;
+ }
+
+ return (int)$items[0];
+ }
+
+ /**
+ * findNextJob
+ *
+ * @throws \RuntimeException
+ *
+ * @return null|Job
+ */
+ protected function findNextJob(): ?Job
+ {
+ $next_at_timestamp = $this->getNextAtTimestamp();
+ $cron_id = $this->getNextCronId();
+
+ if (is_null($cron_id)) {
+ return $this->getNextAtJob();
+ }
+
+ $next_cron_timestamp = $this->getCronTimestamp($cron_id);
+
+ if ($next_at_timestamp <= $next_cron_timestamp) {
+ return $this->getNextAtJob();
+ }
+
+ return $this->getNextCronJob();
+ }
+
+ /**
+ * getNextAtJob
+ *
+ * @return null|Job
+ */
+ protected function getNextAtJob(): ?Job
+ {
+ $next_timestamp = $this->getNextAtTimestamp();
+
+ if (!is_null($next_timestamp)) {
+ return $this->getNextJobAtTimestamp($next_timestamp);
+ }
+
+ return null;
+ }
+
+ /**
+ * hasAtJobsToProcess
+ *
+ * @return bool
+ */
+ private function hasAtJobsToProcess(): bool
+ {
+ return !is_null($this->getNextAtTimestamp());
+ }
+
+ /**
+ * hasCronJobsToProcess
+ *
+ * @return bool
+ */
+ private function hasCronJobsToProcess(): bool
+ {
+ return !is_null($this->getNextCronId());
+ }
+
+ /**
+ * getNextCronId
+ */
+ private function getNextCronId()
+ {
+ $at = time();
+
+ $items = $this->client->zrangebyscore(self::CRON_QUEUE_NAME, '-inf', $at, ['limit', 0, 1]);
+
+ if (empty($items)) {
+ return;
+ }
+
+ return $items[0];
+ }
+
+ /**
+ * getNextJobAtTimestamp
+ *
+ * @param int $timestamp
+ *
+ * @return Job
+ */
+ private function getNextJobAtTimestamp(int $timestamp): Job
+ {
+ $key = sprintf('%s:%s', self::AT_QUEUE_NAME, $timestamp);
+
+ $item = json_decode($this->client->lpop($key), true);
+
+ $this->cleanupTimestamp($timestamp);
+
+ return new Job($item['class'], $item['queue'], $item['args'], $item['id']);
+ }
+
+ /**
+ * cleanupTimestamp
+ *
+ * @param int $timestamp
+ */
+ private function cleanupTimestamp(int $timestamp): void
+ {
+ $key = sprintf('%s:%s', self::AT_QUEUE_NAME, $timestamp);
+
+ if (!$this->client->llen($key)) {
+ $this->client->del([$key]);
+ $this->client->zrem(self::AT_QUEUE_NAME, $timestamp);
+ }
+ }
+
+ /**
+ * getCronTimestamp
+ *
+ * @param string $cron_id
+ *
+ * @return int|null
+ */
+ private function getCronTimestamp(string $cron_id): ?int
+ {
+ $items = $this->client->zscore(self::CRON_QUEUE_NAME, $cron_id);
+
+ if (empty($items)) {
+ return null;
+ }
+
+ return (int)$items[0];
+ }
+
+ /**
+ * getNextCronJob
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ *
+ * @throws \RuntimeException
+ *
+ * @return null|Job
+ */
+ private function getNextCronJob(): ?Job
+ {
+ $cron_id = $this->getNextCronId();
+
+ if (is_null($cron_id)) {
+ return null;
+ }
+
+ $key = sprintf('%s:%s', self::CRON_QUEUE_NAME, $cron_id);
+
+ $data = json_decode($this->client->get($key), true);
+
+ $this->updateCron($cron_id, $data['cron']);
+
+ $job = $data['job'];
+
+ return new Job($job['class'], $job['queue'], $job['args']);
+ }
+
+ /**
+ * updateCron
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ *
+ * @param string $id
+ * @param string $cron
+ *
+ * @throws \RuntimeException
+ */
+ private function updateCron(string $id, string $cron)
+ {
+ $cronExpression = CronExpression::factory($cron);
+ $next_run = $cronExpression->getNextRunDate()->getTimestamp();
+
+ $this->client->zadd(self::CRON_QUEUE_NAME, [$id => $next_run]);
+ }
+}
diff --git a/src/Notification/ConfigProvider.php b/src/Notification/ConfigProvider.php
new file mode 100644
index 0000000..ce2ac58
--- /dev/null
+++ b/src/Notification/ConfigProvider.php
@@ -0,0 +1,93 @@
+ $this->getDependencies(),
+ 'templates' => $this->getTemplates(),
+ ];
+ }
+
+ /**
+ * Returns the container dependencies
+ */
+ public function getDependencies(): array
+ {
+ return [
+ 'delegators' => [
+ SwooleServer::class => [
+ SwooleWorkerDelegator::class
+ ]
+ ],
+ 'factories' => [
+ PsrLoggerAdapter::class => QueueLoggerAdapterFactory::class,
+ SwooleWorker::class => SwooleWorkerFactory::class,
+ NotificationHandler::class => NotificationHandlerFactory::class,
+ ProcessEmail::class => ProcessEmailFactory::class,
+ ProcessNotification::class => ProcessNotificationFactory::class,
+ ProcessRequest::class => ProcessRequestFactory::class,
+ \QueueJitsu\Job\Adapter\RedisAdapter::class => \QueueJitsu\Cli\Job\RedisAdapterFactory::class,
+ \QueueJitsu\Queue\Adapter\RedisAdapter::class => \QueueJitsu\Cli\Queue\RedisAdapterFactory::class,
+ \QueueJitsu\Worker\Adapter\RedisAdapter::class => \QueueJitsu\Cli\Worker\RedisAdapterFactory::class,
+ \QueueJitsu\Scheduler\Worker\Worker::class => \QueueJitsu\Scheduler\Worker\WorkerFactory::class,
+ \Notification\Adapter\RedisAdapter::class => RedisAdapterFactory::class,
+ ],
+ 'aliases' => [
+ LoggerInterface::class => PsrLoggerAdapter::class,
+ \QueueJitsu\Job\Adapter\AdapterInterface::class => \QueueJitsu\Job\Adapter\RedisAdapter::class,
+ \QueueJitsu\Queue\Adapter\AdapterInterface::class => \QueueJitsu\Queue\Adapter\RedisAdapter::class,
+ \QueueJitsu\Worker\Adapter\AdapterInterface::class => \QueueJitsu\Worker\Adapter\RedisAdapter::class,
+ \QueueJitsu\Scheduler\Adapter\AdapterInterface::class => \Notification\Adapter\RedisAdapter::class
+ ]
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public function getTemplates(): array
+ {
+ return [
+ 'paths' => [
+ 'layout' => [__DIR__ . '/../../templates/layout'],
+ 'notification-email' => [__DIR__ . '/../../templates/notification/email'],
+ 'error' => [__DIR__ . '/../../templates/error'],
+ ],
+ ];
+ }
+}
diff --git a/src/Notification/Delegator/SwooleWorkerDelegator.php b/src/Notification/Delegator/SwooleWorkerDelegator.php
new file mode 100644
index 0000000..785a201
--- /dev/null
+++ b/src/Notification/Delegator/SwooleWorkerDelegator.php
@@ -0,0 +1,54 @@
+get(LoggerInterface::class);
+
+ $this->jobManager = $container->get(JobManager::class);
+ $server->on('task', $container->get(SwooleWorker::class));
+
+ $server->on('connect', function ($server, $fd) {
+ echo "Client : {$fd} Connect.\n";
+ });
+ $server->on('message', function ($server, $frame) {
+ echo "received message: {$frame->data}\n";
+// $server->push($frame->fd, json_encode(["hello", "world"]));
+ });
+
+ // Register the function for the event `receive`
+ $server->on('receive', function ($server, $fd, $from_id, $data) use ($logger) {
+ // add raw job here
+ $this->jobManager->enqueue(new Job(ProcessRequest::class, 'requests', [$data]));
+ $logger->notice("Request added to queue.\n");
+ });
+
+ // Register the function for the event `close`
+ $server->on('close', function ($server, $fd) {
+ echo "Client: {$fd} close.\n";
+ });
+
+ $server->on('finish', function ($server, $taskId, $data) use ($logger) {
+ $logger->notice('Task #{taskId} has finished processing', ['taskId' => $taskId]);
+ });
+
+ return $server;
+ }
+}
diff --git a/src/Notification/Factory/NotificationHandlerFactory.php b/src/Notification/Factory/NotificationHandlerFactory.php
new file mode 100644
index 0000000..84b1b55
--- /dev/null
+++ b/src/Notification/Factory/NotificationHandlerFactory.php
@@ -0,0 +1,20 @@
+get('config')['mail'], $container->get('config')['application']);
+ }
+}
diff --git a/src/Notification/Factory/ProcessEmailFactory.php b/src/Notification/Factory/ProcessEmailFactory.php
new file mode 100644
index 0000000..57049b4
--- /dev/null
+++ b/src/Notification/Factory/ProcessEmailFactory.php
@@ -0,0 +1,42 @@
+get('config')['mail'];
+ $options = new SmtpOptions($config);
+ $transport->setOptions($options);
+ return new ProcessEmail(
+ $transport,
+ new Message(),
+ $container->get(TwigRenderer::class),
+ new Body(),
+ new Part(),
+ new Part(),
+ $container->get('config')
+ );
+ }
+}
diff --git a/src/Notification/Factory/ProcessNotificationFactory.php b/src/Notification/Factory/ProcessNotificationFactory.php
new file mode 100644
index 0000000..f6e5cf7
--- /dev/null
+++ b/src/Notification/Factory/ProcessNotificationFactory.php
@@ -0,0 +1,26 @@
+get('config')['notification']
+ );
+ }
+}
diff --git a/src/Notification/Factory/ProcessRequestFactory.php b/src/Notification/Factory/ProcessRequestFactory.php
new file mode 100644
index 0000000..11aa40c
--- /dev/null
+++ b/src/Notification/Factory/ProcessRequestFactory.php
@@ -0,0 +1,29 @@
+get(JobManager::class),
+ $container->get(Scheduler::class)
+ );
+ }
+}
diff --git a/src/Notification/Factory/QueueLoggerAdapterFactory.php b/src/Notification/Factory/QueueLoggerAdapterFactory.php
new file mode 100644
index 0000000..e6a0c55
--- /dev/null
+++ b/src/Notification/Factory/QueueLoggerAdapterFactory.php
@@ -0,0 +1,26 @@
+get('queue_log')
+ );
+ }
+}
diff --git a/src/Notification/Factory/RedisAdapterFactory.php b/src/Notification/Factory/RedisAdapterFactory.php
new file mode 100644
index 0000000..d2d9302
--- /dev/null
+++ b/src/Notification/Factory/RedisAdapterFactory.php
@@ -0,0 +1,27 @@
+get(Client::class);
+
+ return new RedisAdapter($redis);
+ }
+}
diff --git a/src/Notification/Factory/SwooleWorkerFactory.php b/src/Notification/Factory/SwooleWorkerFactory.php
new file mode 100644
index 0000000..1738a8d
--- /dev/null
+++ b/src/Notification/Factory/SwooleWorkerFactory.php
@@ -0,0 +1,27 @@
+get(LoggerInterface::class)
+ );
+ }
+}
diff --git a/src/Notification/Handler/NotificationHandler.php b/src/Notification/Handler/NotificationHandler.php
new file mode 100644
index 0000000..ae6dd2c
--- /dev/null
+++ b/src/Notification/Handler/NotificationHandler.php
@@ -0,0 +1,49 @@
+config = $config;
+ $this->configFrom = $configFrom;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $data = [
+ 'type' => 'email',
+ 'to' => 'team@dotkernel.com',
+ 'subject' => 'This is a test',
+ 'body' => "This is a test email.\n Thanks",
+ 'config' => $this->config,
+ 'application' => $this->configFrom
+ ];
+ \Resque::enqueue('requests', ProcessRequest::class, $data);
+ return new JsonResponse([
+ 'taskIdentifier' => $data,
+ ]);
+ }
+}
diff --git a/src/Notification/Jobs/ProcessEmail.php b/src/Notification/Jobs/ProcessEmail.php
new file mode 100644
index 0000000..6b32d99
--- /dev/null
+++ b/src/Notification/Jobs/ProcessEmail.php
@@ -0,0 +1,145 @@
+transporter = $transporter;
+ $this->message = $message;
+ $this->config = $config;
+ $this->twigRenderer = $twigRenderer;
+ $this->html = $html;
+ $this->body = $body;
+ $this->text = $text;
+ }
+
+ /**
+ * @param mixed ...$args
+ * @throws EntityNotFoundException
+ * @throws \Doctrine\ORM\NonUniqueResultException
+ * @throws \Twig\Error\LoaderError
+ * @throws \Twig\Error\RuntimeError
+ * @throws \Twig\Error\SyntaxError
+ */
+ public function __invoke(...$args)
+ {
+ $this->args = json_decode($args[0], true);
+ $this->html->setType(Mime::TYPE_HTML);
+ $this->text->setType(Mime::TYPE_TEXT);
+ $this->perform();
+ }
+
+ /**
+ * @throws EntityNotFoundException
+ * @throws \Doctrine\ORM\NonUniqueResultException
+ * @throws \Twig\Error\LoaderError
+ * @throws \Twig\Error\RuntimeError
+ * @throws \Twig\Error\SyntaxError
+ */
+ public function perform()
+ {
+ echo "Start processing email: " . $this->args['type'] . "\n \n";
+
+ switch ($this->args['type']) {
+ case 'password-reset':
+ $this->preparePasswordResetEmail();
+ break;
+ case 'overnight-report':
+ $this->prepareReportEmail();
+ break;
+ }
+
+ $this->sendEmail();
+
+ echo "End processing email: " . $this->args['type'] . "\n \n";
+ }
+
+ /**
+ * @throws EntityNotFoundException
+ */
+ public function sendEmail()
+ {
+ $this->message->setTo('email@receiver.com');
+ $this->message->setFrom('pingu@apidemia.com', 'Pingu');
+ $this->body->setParts([$this->html, $this->text]);
+ $this->message->setBody($this->body);
+
+ try {
+ $this->transporter->send($this->message);
+ } catch (\Exception $e) {
+ echo 'Caught exception: ', $e->getMessage(), "\n";
+ }
+ }
+
+ /**
+ * @throws \Twig\Error\LoaderError
+ * @throws \Twig\Error\RuntimeError
+ * @throws \Twig\Error\SyntaxError
+ */
+ public function prepareReportEmail()
+ {
+ $this->message->setSubject('Daily report - ' . date('Y-m-d'));
+ $this->html->setContent($this->twigRenderer->render('notification-email::report/new'));
+ }
+
+ /**
+ * @throws \Twig\Error\LoaderError
+ * @throws \Twig\Error\RuntimeError
+ * @throws \Twig\Error\SyntaxError
+ */
+ public function preparePasswordResetEmail()
+ {
+ $this->message->setSubject('Reset your password');
+ $this->html->setContent($this->twigRenderer->render('notification-email::user/reset-password-requested'));
+ }
+}
diff --git a/src/Notification/Jobs/ProcessNotification.php b/src/Notification/Jobs/ProcessNotification.php
new file mode 100644
index 0000000..cc44879
--- /dev/null
+++ b/src/Notification/Jobs/ProcessNotification.php
@@ -0,0 +1,48 @@
+args = json_decode($args[0], true);
+ $this->perform();
+ }
+
+ /**
+ * ProcessNotification constructor.
+ * @param $config
+ */
+ public function __construct($config)
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * Add notification.
+ * @return void
+ */
+ public function perform()
+ {
+ echo "Start processing notification: " . $this->args['type'] . "\n \n";
+ switch ($this->args['type']) {
+ case 'new-friend-request':
+ $this->sendNewFriendNotification();
+ break;
+ }
+ }
+
+ public function sendNewFriendNotification()
+ {
+ // TODO: Implement sendNewFriendNotification() method.
+ }
+}
diff --git a/src/Notification/Jobs/ProcessRequest.php b/src/Notification/Jobs/ProcessRequest.php
new file mode 100644
index 0000000..ed4823f
--- /dev/null
+++ b/src/Notification/Jobs/ProcessRequest.php
@@ -0,0 +1,101 @@
+args = json_decode($args[0], true);
+ $this->perform();
+ }
+
+ /**
+ * ProcessRequest constructor.
+ * @param JobManager $jobManager
+ * @param Scheduler $scheduler
+ */
+ public function __construct(
+ JobManager $jobManager,
+ Scheduler $scheduler
+ ) {
+ $this->jobManager = $jobManager;
+ $this->jobScheduler = $scheduler;
+ }
+
+ /**
+ * Add requests in queues.
+ * @return void
+ */
+ public function perform()
+ {
+ echo "Start processing request: " . $this->args['type'] . "\n \n";
+ switch ($this->args['type']) {
+ case 'password-reset':
+ $this->setEmailEnqueue();
+ break;
+ case 'new-friend-request':
+ $this->setNotificationEnqueue();
+ break;
+ case 'overnight-report':
+ // for delaying the email just use methods with setDelayed giving parameter the
+ // strtotime() moment when the notification should be send
+ $this->setDelayedEmailEnqueue(strtotime('in 1h'));
+ // same for sending notification
+ $this->setDelayedNotificationEnqueue(strtotime('today 7:00 am'));
+ break;
+ default:
+ $this->setNotificationEnqueue();
+ $this->setEmailEnqueue();
+ break;
+ }
+ echo "End processing request: " . $this->args['type'] . "\n \n";
+ }
+
+ private function setNotificationEnqueue()
+ {
+ $this->jobManager->enqueue(new Job(ProcessNotification::class,'push', [json_encode($this->args)]));
+ }
+
+ private function setEmailEnqueue()
+ {
+ $this->jobManager->enqueue(
+ new Job(ProcessEmail::class, 'email', [json_encode($this->args)])
+ );
+ }
+
+ private function setDelayedNotificationEnqueue(int $delay)
+ {
+ $this->jobScheduler->enqueueAt(
+ $delay,
+ new Job(
+ ProcessNotification::class,
+ 'push',
+ [json_encode($this->args)]
+ )
+ );
+ }
+
+ private function setDelayedEmailEnqueue(int $delay)
+ {
+ $this->jobScheduler->enqueueAt(
+ $delay,
+ new Job(ProcessEmail::class, 'email', [json_encode($this->args)])
+ );
+ }
+}
diff --git a/src/Notification/Logger/NotificationLogger.php b/src/Notification/Logger/NotificationLogger.php
new file mode 100644
index 0000000..6801b1c
--- /dev/null
+++ b/src/Notification/Logger/NotificationLogger.php
@@ -0,0 +1,101 @@
+log(LogLevel::EMERGENCY, $message, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function alert($message, array $context = [])
+ {
+ $this->log(LogLevel::ALERT, $message, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function critical($message, array $context = [])
+ {
+ $this->log(LogLevel::CRITICAL, $message, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function error($message, array $context = [])
+ {
+ $this->log(LogLevel::ERROR, $message, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function warning($message, array $context = [])
+ {
+ $this->log(LogLevel::WARNING, $message, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function notice($message, array $context = [])
+ {
+ $this->log(LogLevel::NOTICE, $message, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function info($message, array $context = [])
+ {
+ $this->log(LogLevel::INFO, $message, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function debug($message, array $context = [])
+ {
+ $this->log(LogLevel::DEBUG, $message, $context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function log($level, $message, array $context = [])
+ {
+ foreach ($context as $key => $value) {
+ $search = sprintf('{%s}', $key);
+ $message = str_replace($search, $value, $message);
+ }
+ printf('%s%s', $message, PHP_EOL);
+ }
+}
diff --git a/src/Notification/Middleware/DefaultTemplateParamsMiddleware.php b/src/Notification/Middleware/DefaultTemplateParamsMiddleware.php
new file mode 100644
index 0000000..47d2972
--- /dev/null
+++ b/src/Notification/Middleware/DefaultTemplateParamsMiddleware.php
@@ -0,0 +1,59 @@
+template = $template;
+ $this->apiUrl = $apiUrl;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ * @param RequestHandlerInterface $handler
+ * @return mixed|ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $this->template->addDefaultParam(
+ TemplateRendererInterface::TEMPLATE_ALL,
+ 'api_url',
+ rtrim($this->apiUrl, '/')
+ );
+
+ return $handler->handle($request);
+ }
+}
diff --git a/src/Notification/Worker/SwooleWorker.php b/src/Notification/Worker/SwooleWorker.php
new file mode 100644
index 0000000..1fff226
--- /dev/null
+++ b/src/Notification/Worker/SwooleWorker.php
@@ -0,0 +1,57 @@
+logger = $logger;
+ }
+
+ public function __invoke($server, $taskId, $fromId, $data)
+ {
+// if (!$data instanceof Array_) {
+// $this->logger->error('Invalid data type provided to task worker: {type}', [
+// 'type' => is_object($data) ? get_class($data) : gettype($data)
+// ]);
+// return;
+// }
+
+ $this->logger->notice('Starting work on task {taskId} using data: {data}', [
+ 'taskId' => $taskId,
+ 'data' => json_encode($data),
+ ]);
+
+ try {
+ if ($this->redis->hasItem($data['taskKey'])) {
+ $this->logger->notice('Received success taskId {taskId}, redisValue {redisKey} using data: {data}', [
+ 'taskId' => $taskId,
+ 'redisKey' => $this->redis->getItem($data['taskKey']),
+ 'data' => json_encode($data),
+ ]);
+ } else {
+ $this->redis->setItem($data['taskKey'], $data['taskValue']);
+ $this->logger->notice('Insert success taskId {taskId}, redisValue {redisKey} using data: {data}', [
+ 'taskId' => $taskId,
+ 'redisKey' => $data['taskValue'],
+ 'data' => json_encode($data),
+ ]);
+ }
+ } catch (Throwable $e) {
+ $this->logger->error('Error processing task {taskId}: {error}', [
+ 'taskId' => $taskId,
+ 'error' => $e->getTraceAsString(),
+ ]);
+ }
+
+ // Notify the server that processing of the task has finished:
+ $server->finish('');
+ }
+}
diff --git a/src/Swoole/Command/IsRunningTrait.php b/src/Swoole/Command/IsRunningTrait.php
new file mode 100644
index 0000000..fcf3fc9
--- /dev/null
+++ b/src/Swoole/Command/IsRunningTrait.php
@@ -0,0 +1,33 @@
+pidManager->read();
+
+ if ([] === $pids) {
+ return false;
+ }
+
+ [$masterPid, $managerPid] = $pids;
+
+ if ($managerPid) {
+ // Swoole process mode
+ return $masterPid && $managerPid && SwooleProcess::kill((int) $managerPid, 0);
+ }
+
+ // Swoole base mode, no manager process
+ return $masterPid && SwooleProcess::kill((int) $masterPid, 0);
+ }
+}
diff --git a/src/Swoole/Command/ReloadCommand.php b/src/Swoole/Command/ReloadCommand.php
new file mode 100644
index 0000000..5c28005
--- /dev/null
+++ b/src/Swoole/Command/ReloadCommand.php
@@ -0,0 +1,97 @@
+serverMode = $serverMode;
+ parent::__construct($name);
+ }
+
+ protected function configure() : void
+ {
+ $this->setDescription('Reload the web server.');
+ $this->setHelp(self::HELP);
+ $this->addOption(
+ 'num-workers',
+ 'w',
+ InputOption::VALUE_REQUIRED,
+ 'Number of worker processes to use after reloading.'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) : int
+ {
+ if ($this->serverMode !== SWOOLE_PROCESS) {
+ $output->writeln(
+ 'Server is not configured to run in SWOOLE_PROCESS mode;'
+ . ' cannot reload'
+ );
+ return 1;
+ }
+
+ $output->writeln('Reloading server ...');
+
+ $application = $this->getApplication();
+
+ $stop = $application->find('stop');
+ $result = $stop->run(new ArrayInput([
+ 'command' => 'stop',
+ ]), $output);
+
+ if (0 !== $result) {
+ $output->writeln('Cannot reload server: unable to stop current server');
+ return $result;
+ }
+
+ $output->write('Waiting for 5 seconds to ensure server is stopped...');
+ for ($i = 0; $i < 5; $i += 1) {
+ $output->write('.');
+ sleep(1);
+ }
+ $output->writeln('[DONE]');
+ $output->writeln('Starting server');
+
+ $start = $application->find('start');
+ $result = $start->run(new ArrayInput([
+ 'command' => 'start',
+ '--daemonize' => true,
+ '--num-workers' => $input->getOption('num-workers') ?? StartCommand::DEFAULT_NUM_WORKERS,
+ ]), $output);
+
+ if (0 !== $result) {
+ $output->writeln('Cannot reload server: unable to start server');
+ return $result;
+ }
+
+ return 0;
+ }
+}
diff --git a/src/Swoole/Command/ReloadCommandFactory.php b/src/Swoole/Command/ReloadCommandFactory.php
new file mode 100644
index 0000000..4da3c36
--- /dev/null
+++ b/src/Swoole/Command/ReloadCommandFactory.php
@@ -0,0 +1,20 @@
+has('config') ? $container->get('config') : [];
+ $mode = $config['dot-swoole']['swoole-server']['mode'] ?? SWOOLE_BASE;
+
+ return new ReloadCommand($mode);
+ }
+}
diff --git a/src/Swoole/Command/StartCommand.php b/src/Swoole/Command/StartCommand.php
new file mode 100644
index 0000000..45e9dc7
--- /dev/null
+++ b/src/Swoole/Command/StartCommand.php
@@ -0,0 +1,92 @@
+container = $container;
+ parent::__construct($name);
+ }
+
+ protected function configure(): void
+ {
+ $this->setDescription('Start the web server.');
+ $this->setHelp(self::HELP);
+ $this->addOption(
+ 'daemonize',
+ 'd',
+ InputOption::VALUE_NONE,
+ 'Daemonize the web server (run as a background process).'
+ );
+ $this->addOption(
+ 'num-workers',
+ 'w',
+ InputOption::VALUE_REQUIRED,
+ 'Number of worker processes to use.'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->pidManager = $this->container->get(PidManager::class);
+ if ($this->isRunning()) {
+ $output->writeln('Server is already running!');
+ return 1;
+ }
+
+ $server = $this->container->get(SwooleServer::class);
+ $config = $this->container->get('config');
+ $processName = $config['dot-swoole']['swoole-server']['process-name']
+ ?? self::DEFAULT_PROCESS_NAME;
+
+ $pidManager = $this->pidManager;
+ $server->on('start', function () use ($server, $pidManager, $processName) {
+ $pidManager->write($server->master_pid, $server->manager_pid);
+
+ swoole_set_process_name(sprintf('%s-master', $processName));
+ });
+
+ $server->start();
+
+ return 0;
+ }
+}
diff --git a/src/Swoole/Command/StartCommandFactory.php b/src/Swoole/Command/StartCommandFactory.php
new file mode 100644
index 0000000..ee75f33
--- /dev/null
+++ b/src/Swoole/Command/StartCommandFactory.php
@@ -0,0 +1,15 @@
+pidManager = $pidManager;
+ parent::__construct($name);
+ }
+
+ protected function configure() : void
+ {
+ $this->setDescription('Get the status of the web server.');
+ $this->setHelp(self::HELP);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) : int
+ {
+ $message = $this->isRunning()
+ ? 'Server is running'
+ : 'Server is not running';
+
+ $output->writeln($message);
+
+ return 0;
+ }
+}
diff --git a/src/Swoole/Command/StatusCommandFactory.php b/src/Swoole/Command/StatusCommandFactory.php
new file mode 100644
index 0000000..8970521
--- /dev/null
+++ b/src/Swoole/Command/StatusCommandFactory.php
@@ -0,0 +1,16 @@
+get(PidManager::class));
+ }
+}
diff --git a/src/Swoole/Command/StopCommand.php b/src/Swoole/Command/StopCommand.php
new file mode 100644
index 0000000..50b787f
--- /dev/null
+++ b/src/Swoole/Command/StopCommand.php
@@ -0,0 +1,104 @@
+killProcess = Closure::fromCallable([SwooleProcess::class, 'kill']);
+ $this->pidManager = $pidManager;
+ parent::__construct($name);
+ }
+
+ protected function configure() : void
+ {
+ $this->setDescription('Stop the web server.');
+ $this->setHelp(self::HELP);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) : int
+ {
+ if (! $this->isRunning()) {
+ $output->writeln('Server is not running');
+ return 0;
+ }
+
+ $output->writeln('Stopping server ...');
+
+ if (! $this->stopServer()) {
+ $output->writeln('Error stopping server; check logs for details');
+ return 1;
+ }
+
+ $output->writeln('Server stopped');
+ return 0;
+ }
+
+ private function stopServer() : bool
+ {
+ [$masterPid, ] = $this->pidManager->read();
+ $startTime = time();
+ $result = ($this->killProcess)((int) $masterPid);
+
+ while (! $result) {
+ if (! ($this->killProcess)((int) $masterPid, 0)) {
+ continue;
+ }
+ if (time() - $startTime >= $this->waitThreshold) {
+ $result = false;
+ break;
+ }
+ usleep(10000);
+ }
+
+ if (! $result) {
+ return false;
+ }
+
+ $this->pidManager->delete();
+
+ return true;
+ }
+}
diff --git a/src/Swoole/Command/StopCommandFactory.php b/src/Swoole/Command/StopCommandFactory.php
new file mode 100644
index 0000000..5c7e2ec
--- /dev/null
+++ b/src/Swoole/Command/StopCommandFactory.php
@@ -0,0 +1,16 @@
+get(PidManager::class));
+ }
+}
diff --git a/src/Swoole/ConfigProvider.php b/src/Swoole/ConfigProvider.php
new file mode 100644
index 0000000..c7d90e3
--- /dev/null
+++ b/src/Swoole/ConfigProvider.php
@@ -0,0 +1,59 @@
+ $this->getDependencies()]
+ : [];
+
+ $config['dot-swoole'] = $this->getDefaultConfig();
+
+ return $config;
+ }
+
+ public function getDefaultConfig() : array
+ {
+ return [
+ 'swoole-server' => [
+ 'options' => [
+ // We set a default for this. Without one, Swoole\Server
+ // defaults to the value of `ulimit -n`. Unfortunately, in
+ // virtualized or containerized environments, this often
+ // reports higher than the host container allows. 1024 is a
+ // sane default; users should check their host system, however,
+ // and set a production value to match.
+ 'max_conn' => 1024,
+ ],
+ 'static-files' => [
+ 'enable' => true,
+ ],
+ ],
+ ];
+ }
+
+ public function getDependencies() : array
+ {
+ return [
+ 'factories' => [
+ Command\ReloadCommand::class => Command\ReloadCommandFactory::class,
+ Command\StartCommand::class => Command\StartCommandFactory::class,
+ Command\StatusCommand::class => Command\StatusCommandFactory::class,
+ Command\StopCommand::class => Command\StopCommandFactory::class,
+ PidManager::class => PidManagerFactory::class,
+ SwooleServer::class => ServerFactory::class,
+ ]
+ ];
+ }
+}
diff --git a/src/Swoole/Exception/ExceptionInterface.php b/src/Swoole/Exception/ExceptionInterface.php
new file mode 100644
index 0000000..cdab75c
--- /dev/null
+++ b/src/Swoole/Exception/ExceptionInterface.php
@@ -0,0 +1,14 @@
+get('config');
+ return new PidManager(
+ $config['dot-swoole']['swoole-server']['options']['pid_file']
+ ?? sys_get_temp_dir() . '/dot-swoole.pid'
+ );
+ }
+}
diff --git a/src/Swoole/Factory/ServerFactory.php b/src/Swoole/Factory/ServerFactory.php
new file mode 100644
index 0000000..9414be7
--- /dev/null
+++ b/src/Swoole/Factory/ServerFactory.php
@@ -0,0 +1,96 @@
+get('config');
+ $swooleConfig = $config['dot-swoole'] ?? [];
+ $serverConfig = $swooleConfig['swoole-server'] ?? [];
+
+ $host = $serverConfig['host'] ?? static::DEFAULT_HOST;
+ $port = $serverConfig['port'] ?? static::DEFAULT_PORT;
+ $mode = $serverConfig['mode'] ?? SWOOLE_BASE;
+ $protocol = $serverConfig['protocol'] ?? SWOOLE_SOCK_TCP;
+
+ if ($port < 1 || $port > 65535) {
+ throw new Exception\InvalidArgumentException('Invalid port');
+ }
+
+ if (! in_array($mode, static::MODES, true)) {
+ throw new Exception\InvalidArgumentException('Invalid server mode');
+ }
+
+ $validProtocols = static::PROTOCOLS;
+ if (defined('SWOOLE_SSL')) {
+ $validProtocols[] = SWOOLE_SOCK_TCP | SWOOLE_SSL;
+ $validProtocols[] = SWOOLE_SOCK_TCP6 | SWOOLE_SSL;
+ }
+
+ if (! in_array($protocol, $validProtocols, true)) {
+ throw new Exception\InvalidArgumentException('Invalid server protocol');
+ }
+
+ $enableCoroutine = $swooleConfig['enable_coroutine'] ?? false;
+ if ($enableCoroutine && method_exists(SwooleRuntime::class, 'enableCoroutine')) {
+ SwooleRuntime::enableCoroutine(true);
+ }
+
+ $httpServer = new SwooleServer($host, $port, $mode, $protocol);
+ $serverOptions = $serverConfig['options'] ?? [];
+ $httpServer->set($serverOptions);
+
+ return $httpServer;
+ }
+}
diff --git a/src/Swoole/PidManager.php b/src/Swoole/PidManager.php
new file mode 100644
index 0000000..77335cf
--- /dev/null
+++ b/src/Swoole/PidManager.php
@@ -0,0 +1,69 @@
+pidFile = $pidFile;
+ }
+
+ /**
+ * Write master pid and manager pid to pid file
+ *
+ * @throws RuntimeException When $pidFile is not writable
+ */
+ public function write(int $masterPid, int $managerPid) : void
+ {
+ if (! is_writable($this->pidFile) && ! is_writable(dirname($this->pidFile))) {
+ throw new RuntimeException(sprintf('Pid file "%s" is not writable', $this->pidFile));
+ }
+ file_put_contents($this->pidFile, $masterPid . ',' . $managerPid);
+ }
+
+ /**
+ * Read master pid and manager pid from pid file
+ *
+ * @return string[] {
+ * @var string $masterPid
+ * @var string $managerPid
+ * }
+ */
+ public function read() : array
+ {
+ $pids = [];
+ if (is_readable($this->pidFile)) {
+ $content = file_get_contents($this->pidFile);
+ $pids = explode(',', $content);
+ }
+ return $pids;
+ }
+
+ /**
+ * Delete pid file
+ */
+ public function delete() : bool
+ {
+ if (is_writable($this->pidFile)) {
+ return unlink($this->pidFile);
+ }
+ return false;
+ }
+}
diff --git a/templates/error/error.html.twig b/templates/error/error.html.twig
new file mode 100644
index 0000000..3c0e69b
--- /dev/null
+++ b/templates/error/error.html.twig
@@ -0,0 +1,24 @@
+
Oops!
+This is awkward.
+We encountered a {{ status }} {{ reason }} error.
+{% if status == 404 %}
+ You are looking for something that doesn't exist or may have moved.
+{% endif %}
+
+{% if error %}
+ Error details
+ Message: {{ error.getMessage() }}
+ in {{ error.getFile() }}:{{ error.getLine() }}
+ Trace:
+ {{ error.getTraceAsString() }}
+
+ {% set prev = error.getPrevious() %}
+ {% for i in 1..10000 if prev %}
+ Previous error
+ Message: {{ prev.getMessage() }}
+ in {{ prev.getFile() }}:{{ prev.getLine() }}
+ Trace:
+ {{ prev.getTraceAsString() }}
+ {% set prev = prev.getPrevious() %}
+ {% endfor %}
+{% endif %}
diff --git a/templates/layout/notification-email.html.twig b/templates/layout/notification-email.html.twig
new file mode 100644
index 0000000..92fd4c8
--- /dev/null
+++ b/templates/layout/notification-email.html.twig
@@ -0,0 +1,6 @@
+
+
+
+ {% block content %}{% endblock %}
+
+