From 7cb3bb3932d8baf50582aef9c9265ffe1b9a8d8b Mon Sep 17 00:00:00 2001 From: sergiu Date: Fri, 12 Jun 2020 11:37:20 +0300 Subject: [PATCH] Initial commit --- LICENSE.md | 21 ++ README.md | 157 ++++++++++ bin/clear-config-cache.php | 48 +++ bin/dot-swoole | 68 +++++ composer.json | 115 +++++++ config/.gitignore | 1 + config/autoload/.gitignore | 2 + config/autoload/dependencies.global.php | 26 ++ config/autoload/development.local.php.dist | 39 +++ config/autoload/doctrine.global.php | 47 +++ config/autoload/local.php.dist | 66 ++++ config/autoload/log.global.php | 32 ++ config/autoload/mail.local.php.dist | 20 ++ config/autoload/mezzio.global.php | 25 ++ config/autoload/notification.local.php.dist | 8 + config/autoload/redis.local.php.dist | 15 + config/autoload/swoole.local.php.dist | 37 +++ config/autoload/templates.global.php | 35 +++ config/config.php | 52 ++++ config/container.php | 14 + config/development.config.php.dist | 30 ++ config/pipeline.php | 78 +++++ config/routes.php | 50 +++ data/.gitignore | 4 + data/cache/.gitkeep | 0 phpcs.xml.dist | 20 ++ phpunit.xml.dist | 17 ++ public/.htaccess | 19 ++ public/index.php | 30 ++ src/App/src/ConfigProvider.php | 57 ++++ src/App/src/Handler/HomePageHandler.php | 102 +++++++ .../src/Handler/HomePageHandlerFactory.php | 25 ++ src/App/src/Handler/PingHandler.php | 20 ++ src/Notification/Adapter/RedisAdapter.php | 287 ++++++++++++++++++ src/Notification/ConfigProvider.php | 93 ++++++ .../Delegator/SwooleWorkerDelegator.php | 54 ++++ .../Factory/NotificationHandlerFactory.php | 20 ++ .../Factory/ProcessEmailFactory.php | 42 +++ .../Factory/ProcessNotificationFactory.php | 26 ++ .../Factory/ProcessRequestFactory.php | 29 ++ .../Factory/QueueLoggerAdapterFactory.php | 26 ++ .../Factory/RedisAdapterFactory.php | 27 ++ .../Factory/SwooleWorkerFactory.php | 27 ++ .../Handler/NotificationHandler.php | 49 +++ src/Notification/Jobs/ProcessEmail.php | 145 +++++++++ src/Notification/Jobs/ProcessNotification.php | 48 +++ src/Notification/Jobs/ProcessRequest.php | 101 ++++++ .../Logger/NotificationLogger.php | 101 ++++++ .../DefaultTemplateParamsMiddleware.php | 59 ++++ src/Notification/Worker/SwooleWorker.php | 57 ++++ src/Swoole/Command/IsRunningTrait.php | 33 ++ src/Swoole/Command/ReloadCommand.php | 97 ++++++ src/Swoole/Command/ReloadCommandFactory.php | 20 ++ src/Swoole/Command/StartCommand.php | 92 ++++++ src/Swoole/Command/StartCommandFactory.php | 15 + src/Swoole/Command/StatusCommand.php | 50 +++ src/Swoole/Command/StatusCommandFactory.php | 16 + src/Swoole/Command/StopCommand.php | 104 +++++++ src/Swoole/Command/StopCommandFactory.php | 16 + src/Swoole/ConfigProvider.php | 59 ++++ src/Swoole/Exception/ExceptionInterface.php | 14 + .../Exception/ExtensionNotLoadedException.php | 9 + .../Exception/InvalidArgumentException.php | 9 + .../Exception/InvalidConfigException.php | 9 + ...validStaticResourceMiddlewareException.php | 22 ++ src/Swoole/Exception/RuntimeException.php | 9 + src/Swoole/Factory/PidManagerFactory.php | 22 ++ src/Swoole/Factory/ServerFactory.php | 96 ++++++ src/Swoole/PidManager.php | 69 +++++ templates/error/error.html.twig | 24 ++ templates/layout/notification-email.html.twig | 6 + .../notification/email/report/new.html.twig | 22 ++ .../user/reset-password-requested.html.twig | 22 ++ .../Handler/HomePageHandlerFactoryTest.php | 53 ++++ test/AppTest/Handler/HomePageHandlerTest.php | 65 ++++ test/AppTest/Handler/PingHandlerTest.php | 26 ++ 76 files changed, 3450 insertions(+) create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 bin/clear-config-cache.php create mode 100644 bin/dot-swoole create mode 100644 composer.json create mode 100644 config/.gitignore create mode 100644 config/autoload/.gitignore create mode 100644 config/autoload/dependencies.global.php create mode 100644 config/autoload/development.local.php.dist create mode 100644 config/autoload/doctrine.global.php create mode 100644 config/autoload/local.php.dist create mode 100644 config/autoload/log.global.php create mode 100644 config/autoload/mail.local.php.dist create mode 100644 config/autoload/mezzio.global.php create mode 100644 config/autoload/notification.local.php.dist create mode 100644 config/autoload/redis.local.php.dist create mode 100644 config/autoload/swoole.local.php.dist create mode 100644 config/autoload/templates.global.php create mode 100644 config/config.php create mode 100644 config/container.php create mode 100644 config/development.config.php.dist create mode 100644 config/pipeline.php create mode 100644 config/routes.php create mode 100644 data/.gitignore create mode 100644 data/cache/.gitkeep create mode 100644 phpcs.xml.dist create mode 100644 phpunit.xml.dist create mode 100644 public/.htaccess create mode 100644 public/index.php create mode 100644 src/App/src/ConfigProvider.php create mode 100644 src/App/src/Handler/HomePageHandler.php create mode 100644 src/App/src/Handler/HomePageHandlerFactory.php create mode 100644 src/App/src/Handler/PingHandler.php create mode 100644 src/Notification/Adapter/RedisAdapter.php create mode 100644 src/Notification/ConfigProvider.php create mode 100644 src/Notification/Delegator/SwooleWorkerDelegator.php create mode 100644 src/Notification/Factory/NotificationHandlerFactory.php create mode 100644 src/Notification/Factory/ProcessEmailFactory.php create mode 100644 src/Notification/Factory/ProcessNotificationFactory.php create mode 100644 src/Notification/Factory/ProcessRequestFactory.php create mode 100644 src/Notification/Factory/QueueLoggerAdapterFactory.php create mode 100644 src/Notification/Factory/RedisAdapterFactory.php create mode 100644 src/Notification/Factory/SwooleWorkerFactory.php create mode 100644 src/Notification/Handler/NotificationHandler.php create mode 100644 src/Notification/Jobs/ProcessEmail.php create mode 100644 src/Notification/Jobs/ProcessNotification.php create mode 100644 src/Notification/Jobs/ProcessRequest.php create mode 100644 src/Notification/Logger/NotificationLogger.php create mode 100644 src/Notification/Middleware/DefaultTemplateParamsMiddleware.php create mode 100644 src/Notification/Worker/SwooleWorker.php create mode 100644 src/Swoole/Command/IsRunningTrait.php create mode 100644 src/Swoole/Command/ReloadCommand.php create mode 100644 src/Swoole/Command/ReloadCommandFactory.php create mode 100644 src/Swoole/Command/StartCommand.php create mode 100644 src/Swoole/Command/StartCommandFactory.php create mode 100644 src/Swoole/Command/StatusCommand.php create mode 100644 src/Swoole/Command/StatusCommandFactory.php create mode 100644 src/Swoole/Command/StopCommand.php create mode 100644 src/Swoole/Command/StopCommandFactory.php create mode 100644 src/Swoole/ConfigProvider.php create mode 100644 src/Swoole/Exception/ExceptionInterface.php create mode 100644 src/Swoole/Exception/ExtensionNotLoadedException.php create mode 100644 src/Swoole/Exception/InvalidArgumentException.php create mode 100644 src/Swoole/Exception/InvalidConfigException.php create mode 100644 src/Swoole/Exception/InvalidStaticResourceMiddlewareException.php create mode 100644 src/Swoole/Exception/RuntimeException.php create mode 100644 src/Swoole/Factory/PidManagerFactory.php create mode 100644 src/Swoole/Factory/ServerFactory.php create mode 100644 src/Swoole/PidManager.php create mode 100644 templates/error/error.html.twig create mode 100644 templates/layout/notification-email.html.twig create mode 100644 templates/notification/email/report/new.html.twig create mode 100644 templates/notification/email/user/reset-password-requested.html.twig create mode 100644 test/AppTest/Handler/HomePageHandlerFactoryTest.php create mode 100644 test/AppTest/Handler/HomePageHandlerTest.php create mode 100644 test/AppTest/Handler/PingHandlerTest.php 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 %} + + \ No newline at end of file diff --git a/templates/notification/email/report/new.html.twig b/templates/notification/email/report/new.html.twig new file mode 100644 index 0000000..7a08550 --- /dev/null +++ b/templates/notification/email/report/new.html.twig @@ -0,0 +1,22 @@ +{% extends '@layout/notification-email.html.twig' %} + +{% block content %} + + + + + + + +

Hello there!

+

Here you can see today`s report

+ + + Fancy report link + + +

Happy collaborating,

+

Pingu @Apidemia

+
+{% endblock %} diff --git a/templates/notification/email/user/reset-password-requested.html.twig b/templates/notification/email/user/reset-password-requested.html.twig new file mode 100644 index 0000000..358fbfa --- /dev/null +++ b/templates/notification/email/user/reset-password-requested.html.twig @@ -0,0 +1,22 @@ +{% extends '@layout/notification-email.html.twig' %} + +{% block content %} + + + + + + + +

Hello there!

+

Reset your password by clicking the button bellow

+ + + The button bellow + + +

Happy collaborating,

+

Pingu @Apidemia

+
+{% endblock %} diff --git a/test/AppTest/Handler/HomePageHandlerFactoryTest.php b/test/AppTest/Handler/HomePageHandlerFactoryTest.php new file mode 100644 index 0000000..b9d4c3e --- /dev/null +++ b/test/AppTest/Handler/HomePageHandlerFactoryTest.php @@ -0,0 +1,53 @@ +container = $this->prophesize(ContainerInterface::class); + $router = $this->prophesize(RouterInterface::class); + + $this->container->get(RouterInterface::class)->willReturn($router); + } + + public function testFactoryWithoutTemplate() + { + $factory = new HomePageHandlerFactory(); + $this->container->has(TemplateRendererInterface::class)->willReturn(false); + + $this->assertInstanceOf(HomePageHandlerFactory::class, $factory); + + $homePage = $factory($this->container->reveal()); + + $this->assertInstanceOf(HomePageHandler::class, $homePage); + } + + public function testFactoryWithTemplate() + { + $this->container->has(TemplateRendererInterface::class)->willReturn(true); + $this->container + ->get(TemplateRendererInterface::class) + ->willReturn($this->prophesize(TemplateRendererInterface::class)); + + $factory = new HomePageHandlerFactory(); + + $homePage = $factory($this->container->reveal()); + + $this->assertInstanceOf(HomePageHandler::class, $homePage); + } +} diff --git a/test/AppTest/Handler/HomePageHandlerTest.php b/test/AppTest/Handler/HomePageHandlerTest.php new file mode 100644 index 0000000..c8d334c --- /dev/null +++ b/test/AppTest/Handler/HomePageHandlerTest.php @@ -0,0 +1,65 @@ +container = $this->prophesize(ContainerInterface::class); + $this->router = $this->prophesize(RouterInterface::class); + } + + public function testReturnsJsonResponseWhenNoTemplateRendererProvided() + { + $homePage = new HomePageHandler( + get_class($this->container->reveal()), + $this->router->reveal(), + null + ); + $response = $homePage->handle( + $this->prophesize(ServerRequestInterface::class)->reveal() + ); + + $this->assertInstanceOf(JsonResponse::class, $response); + } + + public function testReturnsHtmlResponseWhenTemplateRendererProvided() + { + $renderer = $this->prophesize(TemplateRendererInterface::class); + $renderer + ->render('app::home-page', Argument::type('array')) + ->willReturn(''); + + $homePage = new HomePageHandler( + get_class($this->container->reveal()), + $this->router->reveal(), + $renderer->reveal() + ); + + $response = $homePage->handle( + $this->prophesize(ServerRequestInterface::class)->reveal() + ); + + $this->assertInstanceOf(HtmlResponse::class, $response); + } +} diff --git a/test/AppTest/Handler/PingHandlerTest.php b/test/AppTest/Handler/PingHandlerTest.php new file mode 100644 index 0000000..627201d --- /dev/null +++ b/test/AppTest/Handler/PingHandlerTest.php @@ -0,0 +1,26 @@ +handle( + $this->prophesize(ServerRequestInterface::class)->reveal() + ); + + $json = json_decode((string) $response->getBody()); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertTrue(isset($json->ack)); + } +}