Commit 97a59465 by yeran

master

0 parents
Showing with 16384 additions and 0 deletions
/.idea/
/vendor/
/ignore/
/tests/resource/app/config/testing/
/tests/root/
/composer.lock
default:
rules:
cyclomaticComplexity: [ 10, 6, 2 ]
maintainabilityIndex: [ 0, 75, 95 ]
failure: average.maintainabilityIndex < 50 or sum.loc > 10000
path:
directory: src
extensions: php
exclude: features|tests
filter:
excluded_paths:
- 'tests/*'
- 'bin/*'
- '*.min.js'
- 'src/Cli/Output/Stream.php'
- 'src/Protocol/StreamWrapperTrait.php'
- 'phwoolcon-package/assets/phwoolcon/simpleStorage-0.2.1.js'
- 'phwoolcon-package/assets/phwoolcon/js-cookie-2.1.4.js'
checks:
php:
remove_extra_empty_lines: true
remove_php_closing_tag: true
remove_trailing_whitespace: true
fix_use_statements:
remove_unused: true
preserve_multiple: false
preserve_blanklines: true
# order_alphabetically: true
fix_php_opening_tag: true
fix_linefeed: true
fix_line_ending: true
fix_identation_4spaces: true
fix_doc_comments: true
build: false
tools:
external_code_coverage:
timeout: 600
runs: 1
language: php
sudo: required
services:
- memcached
- redis-server
- mysql
php:
- '5.5'
- '5.6'
- '7.0'
- '7.1'
- '7.2'
matrix:
fast_finish: true
allow_failures:
- php: '7.2'
mysql:
database: phwoolcon_test
username: travis
encoding: utf8mb4
cache:
directories:
- vendor
- $HOME/.composer/cache
- $HOME/cphalcon
- $HOME/pecl_cache
before_install:
- sudo apt-get update -qq
# Install Beanstalkd (Queue support) and Postfix (SMTP mailer support)
- sudo apt-get install -qq beanstalkd postfix
# Use test mail server
- sudo service postfix stop
- smtp-sink -d "%d.%H.%M.%S" localhost:2500 1000 &
- sudo service beanstalkd restart
- pecl channel-update pecl.php.net
- phpenv config-add tests/travis/php-ext.ini
# Install Extensions
- bash bin/ci-install-extensions
- composer install --prefer-source --no-interaction
before_script:
# Create database
- mysql -uroot -e 'CREATE DATABASE `phwoolcon_test` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci */;'
- mysql -uroot -e "GRANT ALL PRIVILEGES ON *.* TO 'travis'@'%';";
# Print Phalcon and Swoole info
- php --ri phalcon
- php --ri swoole
script:
# Check code style
- vendor/bin/phpcs
# Running unit test
- vendor/bin/phpunit --coverage-clover=coverage.clover
after_success:
# Collect code coverage
# If need cache, check ETAG
# ETAG=( $(curl -IL https://scrutinizer-ci.com/ocular.phar 2>/dev/null | grep -i etag) ); echo ${ETAG[1]}
- |
if [[ "$TRAVIS_PHP_VERSION" == '7.1' ]]; then
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover coverage.clover
fi
branches:
#Don't build tags
except:
- /^v\d/
# Phwoolcon Change Logs
## [v2.0]() (2020-xx-xx)
#### Features:
*
#### Tests:
*
#### Bug Fixes:
*
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2016 - 2017 Christopher CHEN <fishdrowned@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
# Phwoolcon
Phalcon + Swoole
***
本项目的目的是创建一个高性能的 Web 应用程序,既可以运行于传统的 php-fpm
模式下,也可以运行在服务模式下。
在服务模式中,你的应用程序可以减少许多非必要的重复计算,获得极致的性能。
如果在服务模式中出现了 Bug,你可以轻松地关闭服务模式,损失一些性能(但是
仍然很快)换取稳定性,待 Bug 修复后再启用服务模式。
# 使用
## 安装
这是 Phwoolcon 库。
你也可以用 composer 把 Phwoolcon 库加入到你的项目中:
```
composer require phwoolcon/phwoolcon
```
## 代码风格检查
请运行以下脚本:
```
tests/phpcs
```
警告和错误报告会被保存在这个文件里:
```
tests/root/storage/phpcs.txt
```
## 测试
请运行以下脚本:
```
tests/phpunit
```
代码覆盖率报告会以 HTML 格式被保存在这个文件夹里:
```
tests/root/storage/coverage/
```
用浏览器打开 `index.html` 即可阅读报告。
## 配置
请见 [phwoolcon-package/config/](phwoolcon-package/config/)
## 模板
See [phwoolcon-package/views/](phwoolcon-package/views/)
## 静态资源
See [phwoolcon-package/assets/](phwoolcon-package/assets/)
## 翻译/语言
See [phwoolcon-package/locale/](phwoolcon-package/locale/)
## 依赖注入
See [phwoolcon-package/di.php](phwoolcon-package/di.php)
# 主旨
* 关注性能
* 关注可伸缩性
* 提供强大的功能,但是保持直观易读的代码
* 基于组件,显式引入
* 功能可配置
* 代码可测试性
* 规范的代码风格(基于 [PSR-2](http://www.php-fig.org/psr/psr-2/)
# 功能
## 基础组件
* Extended Phalcon Config (Both in native PHP file and DB)
* Phalcon Cache
* Extended Phalcon ORM
* Error Codes
* View: Theme based layouts and templates
* Multiple DB connector
* Events
* Configurable Cookies
* Session
* Openssl based encryption/decryption
* Multiple Queue producer and asynchronous CLI worker
* Assets: Theme based, compilable JS/CSS management
* Log
* Lighten route dispatcher
* Internalization
* Finite state machine
* Simple HTTP client
* Swift Mailer
* Symfony CLI console
# 文档
* [API 参考文档](docs/ApiIndex.md)
# Phwoolcon
Phalcon + Swoole
***
[中文 Readme](README-zh.md)
[Why Do I Start Phwoolcon Project](https://github.com/phwoolcon/phwoolcon/wiki/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%BC%80%E5%8F%91-Phwoolcon)
The purpose of this library is to create a high performance
web application, which can run in traditional php-fpm mode and
service mode.
In service mode, you gain extreme speed for your application,
by reducing lot of unnecessary and repetitive computing.
If you have bugs in service mode, you can easily turn off the service
mode, you loose some speed (but still fast) to gain more stability,
fix your bugs and apply service mode again.
# Usage
## Installation
This is the Phwoolcon library.
to start a new project.
Or add this library to your project by composer:
```
composer require phwoolcon/phwoolcon
```
## Code Style Checking
Please run the following script:
```
tests/phpcs
```
Any warnings or errors will be reported in file:
```
tests/root/storage/phpcs.txt
```
## Testing
Please run the following script:
```
tests/phpunit
```
The code coverage report in HTML format will be generated in folder:
```
tests/root/storage/coverage/
```
To read the report, please open `index.html` in a web browser.
## Configuration
See [phwoolcon-package/config/](phwoolcon-package/config/)
## Templates
See [phwoolcon-package/views/](phwoolcon-package/views/)
## Assets
See [phwoolcon-package/assets/](phwoolcon-package/assets/)
## Locale
See [phwoolcon-package/locale/](phwoolcon-package/locale/)
## Dependency Injection
See [phwoolcon-package/di.php](phwoolcon-package/di.php)
# Spirits
* Aimed at performance
* Aimed at scalability
* Powerful features, with intuitive and readable codes
* Component based, explicitly introduced
* Configurable features
* Code testability
* Follow standard coding style (based on [PSR-2](http://www.php-fig.org/psr/psr-2/))
# Features
## Base Components
* Extended Phalcon Config (Both in native PHP file and DB)
* Phalcon Cache
* Extended Phalcon ORM
* Error Codes
* View: Theme based layouts and templates
* Multiple DB connector
* Events
* Configurable Cookies
* Session
* Openssl based encryption/decryption
* Multiple Queue producer and asynchronous CLI worker
* Assets: Theme based, compilable JS/CSS management
* Log
* Lighten route dispatcher
* Internalization
* Finite state machine
* Simple HTTP client
* Swift Mailer
* Symfony CLI console
# Documents
* [API Reference](docs/ApiIndex.md)
#!/usr/bin/env bash
mkdir tmp-ci-install-extensions
cd tmp-ci-install-extensions
echo $(date +%H:%M:%S) Prepare installers...
echo '{"require":{"techpivot/phalcon-ci-installer":"~1.0","phwoolcon/ci-pecl-cacher":"~1.0"}}' > composer.json
composer install --prefer-source --no-interaction
echo $(date +%H:%M:%S) Installers ready
# Install imagick to pass Phalcon compiling
vendor/bin/ci-pecl-install imagick skip-update
# Install Swoole
vendor/bin/ci-pecl-install swoole
# Install Phalcon
vendor/bin/install-phalcon.sh
cd ..
rm -rf tmp-ci-install-extensions
#!/usr/bin/env bash
cd "$(dirname "$( dirname "${BASH_SOURCE[0]}" )")"
if [ ! -d "ignore/phpdoc" ]; then
mkdir -p ignore/phpdoc
fi
cd ignore/phpdoc
if [ ! -d "vendor" ]; then
composer require phwoolcon/phpdoc-markdown-public
else
file_m_time=`stat -c %Y composer.lock`
current_time=`date +%s`
diff=$(( current_time - file_m_time ))
if [ ${diff} -gt 86400 ]; then
composer update
touch composer.lock
fi
fi
cd ../..
rm -rf docs && ignore/phpdoc/vendor/bin/phpdoc
#!/usr/bin/env bash
cd "$(dirname "$( dirname "${BASH_SOURCE[0]}" )")"
echo Phwoolcon Skeleton Packager
echo
echo Working directory: `pwd`
echo
# Run composer update
composer update phwoolcon/skeleton
echo Copying skeleton from composer...
rm -rf ignore/skeleton
cp -r vendor/phwoolcon/skeleton/ ignore/skeleton
echo Creating tar package...
pushd ignore > /dev/null
rm -rf skeleton/.git
mkdir -p skeleton/phwoolcon-package/assets skeleton/phwoolcon-package/config skeleton/phwoolcon-package/views
tar --owner=0 --group=0 -cf ../src/Cli/Command/Package/skeleton.tar skeleton
popd > /dev/null
ls -lah --color src/Cli/Command/Package/skeleton.tar
echo
echo Done
#!/usr/bin/env php
<?php
/**
* This file will be copied into directory "vendor/bin" after installation
*/
use Phalcon\Text;
use Phalcon\Version;
use Phwoolcon\Cache;
use Phwoolcon\Config;
use Phwoolcon\Db;
use Phwoolcon\DiFix;
use Phwoolcon\Events;
use Phwoolcon\Log;
error_reporting(-1);
$baseDir = dirname(dirname(__DIR__));
if (!is_dir($baseDir . '/vendor/phwoolcon')) {
$baseDir = $_SERVER['PWD'];
}
$vendorDir = $baseDir . '/vendor';
ob_start();
try {
include $baseDir . '/bootstrap/start.php';
$firstRun = !is_file($_SERVER['PHWOOLCON_CONFIG_PATH'] . '/app.php');
} catch (Exception $e) {
$firstRun = true;
}
ob_end_clean();
if ($firstRun) {
$viewPath = $baseDir . '/app/views/';
$assetsBasePath = $baseDir . '/public';
$assetsDir = 'assets';
$assetsCompiledDir = 'static';
$localePath = $baseDir . '/app/locale';
$migrationPath = $baseDir . '/bin/migrations';
if (!function_exists('fileSaveInclude')) {
include $baseDir . '/vendor/phwoolcon/phwoolcon/functions.php';
}
$cachePath = $baseDir . '/storage/cache';
is_dir($cachePath) or mkdir($cachePath, 0755, true);
} else {
$_SERVER['PHWOOLCON_PHALCON_VERSION'] = (int)Version::getId();
Events::register($di);
DiFix::fix($di);
Db::register($di);
Cache::register($di);
Log::register($di);
Config::register($di);
$viewPath = Config::get('view.path', $baseDir . '/app/views/');
$assetsBasePath = Config::get('view.options.assets_options.base_path', $baseDir . '/public');
$assetsDir = Config::get('view.options.assets_options.assets_dir', 'assets');
$assetsCompiledDir = Config::get('view.options.assets_options.compiled_dir', 'static');
$localePath = Config::get('i18n.locale_path', $baseDir . '/app/locale');
$migrationPath = migrationPath();
}
$configPath = $_SERVER['PHWOOLCON_CONFIG_PATH'];
$assetsPath = $assetsBasePath . '/' . $assetsDir;
$assetsCompiledPath = $assetsBasePath . '/' . $assetsCompiledDir;
is_dir($configPath) or mkdir($configPath, 0755, true);
is_dir($assetsPath) or mkdir($assetsPath, 0755, true);
$packages = [];
$packageFiles = detectPhwoolconPackageFiles($vendorDir);
$diFiles = [];
$routeFiles = [];
$commands = [];
$aliases = [];
$assetsGroups = [];
$adminAssetsGroups = [];
if (is_file($migrationCandidatesFile = $vendorDir . '/phwoolcon/migrations.php')) {
$migrationCandidates = include $migrationCandidatesFile;
}
$migrationCandidates['candidates'] = [];
clearConfigFiles($configPath);
clearFolders([
'Assets' => $assetsPath,
'Compiled assets' => $assetsCompiledPath,
'Locales' => $localePath,
'Migrations' => $migrationPath,
'Views' => $viewPath,
]);
echo PHP_EOL;
foreach ($packageFiles as $file) {
if (!is_array($package = include($file))) {
continue;
}
$path = dirname(dirname($file));
foreach ($package as $name => $resources) {
installConfig($name, $path, $configPath);
installViews($name, $path, $viewPath);
installAssets($name, $path, $assetsPath);
installLocale($name, $path, $localePath . '/');
installMigrations($name, $path, $migrationPath . '/');
if (is_dir($packageMigrationPath = $path . '/phwoolcon-package/migrations')) {
$migrationCandidates['candidates'][$name] = $packageMigrationPath;
}
foreach ($resources as $type => $value) {
switch ($type) {
case 'di':
installDi($name, $path, $value, $diFiles);
break;
case 'routes':
installRoutes($name, $path, $value, $routeFiles);
break;
case 'commands':
if (is_array($value)) {
foreach ($value as $sort => $detectedCommands) {
$commands[$sort . '-' . $name] = $detectedCommands;
}
echo sprintf('[%s]%s Commands installed', $name, spacePad($name)), PHP_EOL;
}
break;
case 'class_aliases':
if (is_array($value)) {
foreach ($value as $sort => $detectedAliases) {
$aliases[$sort . '-' . $name] = $detectedAliases;
}
echo sprintf('[%s]%s Class aliases registered', $name, spacePad($name)), PHP_EOL;
}
break;
case 'assets':
if (is_array($value)) {
$assetsGroups = array_merge_recursive($assetsGroups, $value);
echo sprintf('[%s]%s Assets groups registered', $name, spacePad($name)), PHP_EOL;
}
break;
case 'admin_assets':
if (is_array($value)) {
$adminAssetsGroups = array_merge_recursive($adminAssetsGroups, $value);
echo sprintf('[%s]%s Admin assets groups registered', $name, spacePad($name)), PHP_EOL;
}
break;
}
}
}
}
writeDi($diFiles);
writeRoutes($routeFiles);
writeCommands($commands);
writeAliases($aliases);
writeMigrationCandidates($migrationCandidates);
writeAssetsGroups($assetsGroups);
writeAdminAssetsGroups($adminAssetsGroups);
echo PHP_EOL;
generateModelTraits();
try {
if (!$firstRun) {
spl_autoload_register(function ($class) {
throw new Exception("Class '{$class}' not found");
});
include $GLOBALS['vendorDir'] . '/phwoolcon/di.php';
}
} catch (Exception $e) {
}
generateIdeHelper();
function generateIdeHelper()
{
$ideHelperContent = '<?php' . PHP_EOL;
$classAliases = include $GLOBALS['vendorDir'] . '/phwoolcon/class_aliases.php';
ksort($classAliases);
foreach ($classAliases as $alias => $class) {
$classContent = '';
try {
$classContent = method_exists($class, $method = 'ideHelperGenerator') ? $class::{$method}() : '';
} catch (Exception $e) {
}
$ideHelperContent .= "
class {$alias} extends {$class}
{
{$classContent}
}
";
}
$ideHelperPath = $GLOBALS['baseDir'] . '/ignore';
$ideHelperFile = $ideHelperPath . '/_ide_helper.php';
is_dir($ideHelperPath) or mkdir($ideHelperPath, 0755, true);
file_put_contents($ideHelperFile, $ideHelperContent);
echo 'IDE helper generated', PHP_EOL;
}
function generateModelTraits()
{
try {
if ($GLOBALS['firstRun'] || !Config::get('database.default')) {
return;
}
$traitTemplate = <<<'TRAIT'
trait TraitName
{
// protected $_table = 'table_name';
properties
methods
}
TRAIT;
$propertiesTemplate = <<<'PROPERTY'
public $property;
PROPERTY;
$methodsTemplate = <<<'METHOD'
public function getMethod()
{
return $this->property;
}
public function setMethod($value)
{
$this->property = $value;
return $this;
}
METHOD;
$findMethodsTemplate = <<<'FIND'
/**
* @param $value
* @return static[]|\Phalcon\Mvc\Model\Resultset\Simple
*/
public static function findByMethod($value)
{
return static::findSimple([
'property' => $value,
]);
}
/**
* @param $value
* @return static|false
*/
public static function findFirstByMethod($value)
{
return static::findFirstSimple([
'property' => $value,
]);
}
FIND;
$traits = [];
$db = Db::connection();
foreach ($db->listTables() as $table) {
$traitName = Text::camelize($table) . 'ModelTrait';
$properties = [];
$methods = [];
/* @var \Phalcon\Db\Index $index */
$indexedProperties = [];
foreach ($db->describeIndexes($table) as $index) {
$columns = $index->getColumns();
$firstColumn = reset($columns);
$indexedProperties[$firstColumn] = $firstColumn;
}
/* @var \Phalcon\Db\Column $column */
foreach ($db->describeColumns($table) as $column) {
$propertyName = $column->getName();
$properties[] = str_replace('property', $propertyName, $propertiesTemplate);
$methods[] = str_replace($methodsSearch = [
'Method',
'property',
'value',
], $methodsReplacement = [
$methodName = Text::camelize($propertyName),
$propertyName,
lcfirst($methodName),
], $methodsTemplate);
if (isset($indexedProperties[$propertyName])) {
$methods[] = str_replace($methodsSearch, $methodsReplacement, $findMethodsTemplate);
}
}
$traits[] = str_replace([
'table_name',
'TraitName',
'properties',
'methods',
], [
$table,
$traitName,
implode(PHP_EOL, $properties),
implode(PHP_EOL, $methods),
], $traitTemplate);
}
$content = <<<'CONTENT'
if (defined('PHWOOLCON_MODELS_TRAIT_LOADED')) {
return;
}
define('PHWOOLCON_MODELS_TRAIT_LOADED', true);
CONTENT;
$content .= implode(PHP_EOL, $traits);
$target = $GLOBALS['vendorDir'] . '/phwoolcon/model_traits.php';
file_put_contents($target, '<?php' . PHP_EOL . $content);
echo 'Dynamic model traits generated', PHP_EOL;
} catch (Exception $e) {
return;
}
}
function installConfig($package, $path, $configPath)
{
$installed = false;
if ($files = glob($path . '/phwoolcon-package/config/*.php')) {
foreach ($files as $source) {
$file = basename($source);
$destination = $configPath . '/' . $file;
is_file($destination) and unlink($destination);
symlinkRelative($source, $destination);
}
$installed = true;
}
if ($overrides = glob($path . '/phwoolcon-package/config/override-*/*.php')) {
foreach ($overrides as $override) {
$file = basename($override);
$dir = basename(dirname($override));
$destination = $configPath . '/' . $dir . '/' . $file;
is_file($destination) and unlink($destination);
symlinkRelative($override, $destination);
}
$installed = true;
}
if ($installed) {
echo sprintf('[%s]%s Config files installed', $package, spacePad($package)), PHP_EOL;
}
}
function installViews($package, $path, $viewPath)
{
if ($items = glob($path . '/phwoolcon-package/views/*')) {
foreach ($items as $source) {
$destination = $viewPath . basename($source);
symlinkDirOverride($source, $destination);
}
echo sprintf('[%s]%s Views updated', $package, spacePad($package)), PHP_EOL;
}
}
function installAssets($package, $path, $assetsPath)
{
if ($items = glob($path . '/phwoolcon-package/assets/*')) {
foreach ($items as $source) {
$destination = $assetsPath . '/' . basename($source);
symlinkDirOverride($source, $destination);
}
echo sprintf('[%s]%s Assets updated', $package, spacePad($package)), PHP_EOL;
}
}
function installLocale($package, $path, $viewPath)
{
if ($items = glob($path . '/phwoolcon-package/locale/*')) {
foreach ($items as $source) {
$destination = $viewPath . basename($source);
symlinkDirOverride($source, $destination);
}
echo sprintf('[%s]%s Locale updated', $package, spacePad($package)), PHP_EOL;
}
}
function installMigrations($package, $path, $migrationPath)
{
if ($items = glob($path . '/phwoolcon-package/migrations/*')) {
foreach ($items as $source) {
$destination = $migrationPath . basename($source);
is_file($destination) and unlink($destination);
symlinkRelative($source, $destination);
}
echo sprintf('[%s]%s Migrations updated', $package, spacePad($package)), PHP_EOL;
}
}
function installDi($package, $path, $file, &$diFiles = [], $sort = null)
{
if (is_array($file)) {
foreach ($file as $realSort => $realFile) {
installDi($package, $path, $realFile, $diFiles, $realSort);
}
return;
}
echo sprintf('[%s]%s DI registered', $package, spacePad($package)), PHP_EOL;
$diFiles[$sort][] = $path . '/phwoolcon-package/' . $file;
}
function installRoutes($package, $path, $file, &$routeFiles = [], $sort = null)
{
if (is_array($file)) {
foreach ($file as $realSort => $realFile) {
installRoutes($package, $path, $realFile, $routeFiles, $realSort);
}
return;
}
echo sprintf('[%s]%s Routes registered', $package, spacePad($package)), PHP_EOL;
$routeFiles[$sort][] = $path . '/phwoolcon-package/' . $file;
}
function clearConfigFiles($configPath)
{
if ($files = glob($configPath . '/*.php')) {
foreach ($files as $file) {
unlink($file);
}
}
if ($overrideDirs = glob($configPath . '/override-*/')) {
foreach ($overrideDirs as $overrideDir) {
removeDir($overrideDir);
}
}
$label = 'Config';
echo sprintf('[%s]%s files cleared', $label, spacePad($label, 16)), PHP_EOL;
}
function clearFolders(array $folders)
{
foreach ($folders as $label => $path) {
removeDir($path);
mkdir($path, 0755, true);
touch($path . '/.gitkeep');
echo sprintf('[%s]%s folder cleared', $label, spacePad($label, 16)), PHP_EOL;
}
}
function writeDi($diFiles)
{
$target = $GLOBALS['vendorDir'] . '/phwoolcon/di.php';
fileSaveInclude($target, arraySortedMerge($diFiles));
}
function writeRoutes($routeFiles)
{
$target = $GLOBALS['vendorDir'] . '/phwoolcon/routes.php';
fileSaveInclude($target, arraySortedMerge($routeFiles));
}
function writeCommands($commands)
{
$target = $GLOBALS['vendorDir'] . '/phwoolcon/commands.php';
fileSaveArray($target, arraySortedMerge($commands));
}
function writeAliases($aliases)
{
$target = $GLOBALS['vendorDir'] . '/phwoolcon/class_aliases.php';
fileSaveArray($target, arraySortedMerge($aliases));
}
function writeMigrationCandidates($migrationCandidates)
{
$target = $GLOBALS['vendorDir'] . '/phwoolcon/migrations.php';
fileSaveArray($target, $migrationCandidates, function ($content) {
global $baseDir;
$replace = str_replace(['\\', '\\\\'], '/', "'{$baseDir}");
$content = str_replace(['\\', '\\\\'], '/', $content);
return str_replace($replace, "ROOT_PATH . '", $content);
});
}
function writeAssetsGroups($assetsGroups)
{
$target = $GLOBALS['vendorDir'] . '/phwoolcon/assets.php';
fileSaveArray($target, $assetsGroups);
}
function writeAdminAssetsGroups($assetsGroups)
{
$target = $GLOBALS['vendorDir'] . '/phwoolcon/admin_assets.php';
fileSaveArray($target, $assetsGroups);
}
function spacePad($str, $length = 20)
{
$spaces = $length - strlen($str);
return $spaces > 0 ? str_repeat(' ', $spaces) : ' ';
}
{
"name": "phwoolcon/phwoolcon",
"description": "Phalcon + Swoole",
"keywords": ["phwoolcon", "phalcon", "swoole"],
"type": "library",
"license": "Apache-2.0",
"authors": [
{
"name": "Christopher CHEN",
"email": "fishdrowned@gmail.com"
}
],
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=5.5.0",
"ext-curl": "*",
"ext-json": "*",
"ext-openssl": "*",
"ext-PDO": "*",
"ext-mbstring": "*",
"ext-phalcon": "~3.0",
"phwoolcon/fsm": "~1.0",
"phwoolcon/js-polyfills": "~1.0",
"phwoolcon/mail-renderer": "~1.0",
"symfony/console": "~3.0",
"pda/pheanstalk": "~3.1",
"swiftmailer/swiftmailer": "~5.4|~6.0",
"opis/closure": "3.0.1"
},
"require-dev": {
"phwoolcon/test-starter": "~1.0",
"phwoolcon/skeleton": "dev-master"
},
"suggest": {
"ext-swoole": "~1.8|~2.0"
},
"autoload": {
"psr-4": {
"Phwoolcon\\": "src",
"Phwoolcon\\Tests\\": "tests"
},
"exclude-from-classmap": [
"/tests/"
],
"files": ["functions.php"]
},
"bin": [
"bin/update-phwoolcon-package-resource",
"bin/generate-phpdoc"
],
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
}
}
This diff could not be displayed because it is too large.
<?php
use Phalcon\Di;
use Phalcon\Text;
use Phwoolcon\Config;
use Phwoolcon\I18n;
use Phwoolcon\Text as PhwoolconText;
/**
* Translate
*
* @param string $string
* @param array|null $params
* @param string $package
*
* @return string
*/
function __($string, array $params = null, $package = null)
{
if(!$package && $moduleName = Di::getDefault()->getShared('router')->getModuleName()){
$package = $moduleName;
}
return I18n::translate($string, $params, $package);
}
function _e($string, $newLineToBr = true)
{
return PhwoolconText::escapeHtml($string, $newLineToBr);
}
if (!function_exists('array_forget')) {
/**
* Remove an array item from a given array using "dot" notation.
*
* @param array $array
* @param string $key
* @param string $separator
* @return void
*/
function array_forget(&$array, $key, $separator = '.')
{
$keys = explode($separator, $key);
while (count($keys) > 1) {
$key = array_shift($keys);
if (!isset($array[$key]) || !is_array($array[$key])) {
return;
}
$array =& $array[$key];
}
unset($array[array_shift($keys)]);
}
}
if (!function_exists('array_set')) {
/**
* Set an array item to a given value using "dot" notation.
*
* If no key is given to the method, the entire array will be replaced.
*
* @param array $array
* @param string $key
* @param mixed $value
* @param string $separator
* @return array
*/
function array_set(&$array, $key, $value, $separator = '.')
{
if (is_null($key)) {
return $array = $value;
}
$keys = explode($separator, $key);
while (count($keys) > 1) {
$key = array_shift($keys);
// If the key doesn't exist at this depth, we will just create an empty array
// to hold the next value, allowing us to create the arrays to hold final
// values at the correct depth. Then we'll keep digging into the array.
if (!isset($array[$key]) || !is_array($array[$key])) {
$array[$key] = [];
}
$array =& $array[$key];
}
$array[array_shift($keys)] = $value;
return $array;
}
}
/**
* Convert a decimal number into base62 string
*
* @param mixed $val Decimal value
*
* @return string Base 62 value
*/
function base62encode($val)
{
$val = (int)abs($val);
$base = 62;
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$str = '';
do {
$i = $val % $base;
$str = $chars[$i] . $str;
$val = ($val - $i) / $base;
} while ($val > 0);
return $str;
}
/**
* Copy dir, keep destination files, if exists
*
* @param string $source
* @param string $destination
*/
function copyDirMerge($source, $destination)
{
if (is_dir($source)) {
is_dir($destination) or mkdir($destination, 0755, true);
$files = scandir($source);
foreach ($files as $file) {
if ($file != '.' && $file != '..') {
copyDirMerge("$source/$file", "$destination/$file");
}
}
} elseif (file_exists($source) && !file_exists($destination)) {
copy($source, $destination);
}
}
/**
* Copy dir, override destination files, if exists
*
* @param string $source
* @param string $destination
*/
function copyDirOverride($source, $destination)
{
if (is_dir($source)) {
is_dir($destination) or mkdir($destination, 0755, true);
$files = scandir($source);
foreach ($files as $file) {
if ($file != '.' && $file != '..') {
copyDirOverride("$source/$file", "$destination/$file");
}
}
} elseif (file_exists($source)) {
copy($source, $destination);
}
}
/**
* Copy dir, delete entire destination dir first, if exists
*
* @param string $source
* @param string $destination
*/
function copyDirReplace($source, $destination)
{
if (file_exists($destination)) {
removeDir($destination);
}
if (is_dir($source)) {
mkdir($destination, 0755, true);
$files = scandir($source);
foreach ($files as $file) {
if ($file != '.' && $file != '..') {
copyDirReplace("$source/$file", "$destination/$file");
}
}
} elseif (file_exists($source)) {
copy($source, $destination);
}
}
/**
* @param string $vendorDir
* @return array
*/
function detectPhwoolconPackageFiles($vendorDir = null)
{
$vendorDir or $vendorDir = $_SERVER['PHWOOLCON_ROOT_PATH'] . '/vendor';
$packageFiles = glob($vendorDir . '/*/*/phwoolcon-package/*package*.php');
return $packageFiles;
}
/**
* @param string $filename
* @param mixed $array
* @param callable $filter
* @return int
*/
function fileSaveArray($filename, $array, callable $filter = null)
{
$content = var_export($array, true);
$filter and $content = call_user_func($filter, $content);
$result = file_put_contents($filename, '<?php return ' . $content . ';');
opcache_invalidate($filename, true);
return $result;
}
function fileSaveInclude($target, array $includes)
{
$content = '<?php' . PHP_EOL;
$ds = DIRECTORY_SEPARATOR;
foreach ($includes as $file) {
if (Text::startsWith($file, $_SERVER['PHWOOLCON_ROOT_PATH'])) {
$relativePath = str_replace($_SERVER['PHWOOLCON_ROOT_PATH'] . $ds, '/', $file);
$content .= "include ROOT_PATH . '{$relativePath}';" . PHP_EOL;
} // @codeCoverageIgnoreStart
else {
$content .= "include '{$file}';" . PHP_EOL;
}
// @codeCoverageIgnoreEnd
}
$result = file_put_contents($target, $content);
opcache_invalidate($target, true);
return $result;
}
/**
* Safely get child value from an array or an object
*
* Usage:
*
* Assume you want to get value from a multidimensional array like: <code>$array = ['l1' => ['l2' => 'value']]</code>,<br>
* then you can try following:
*
* <code>
* $l1 = fnGet($array, 'l1'); // returns ['l2' => 'value']
* $l2 = fnGet($array, 'l1.l2'); // returns 'value'
* $undefined = fnGet($array, 'l3'); // returns null
* </code>
*
* You can specify default value for undefined keys, and the key separator:
*
* <code>
* $l2 = fnGet($array, 'l1/l2', null, '/'); // returns 'value'
* $undefined = fnGet($array, 'l3', 'default value'); // returns 'default value'
* </code>
*
* @param array|object $array Subject array or object
* @param string $key Indicates the data element of the target value
* @param mixed $default Default value if key not found in subject
* @param string $separator Key level separator, default '.'
* @param bool $hasObject Indicates that the subject may contains object, default false
*
* @return mixed
*/
function fnGet(&$array, $key, $default = null, $separator = '.', $hasObject = false)
{
$tmp =& $array;
if ($hasObject) {
foreach (explode($separator, $key) as $subKey) {
if (isset($tmp->$subKey)) {
$tmp =& $tmp->$subKey;
} else if (is_array($tmp) && isset($tmp[$subKey])) {
$tmp =& $tmp[$subKey];
} else {
return $default;
}
}
return $tmp;
}
foreach (explode($separator, $key) as $subKey) {
if (isset($tmp[$subKey])) {
$tmp =& $tmp[$subKey];
} else {
return $default;
}
}
return $tmp;
}
/**
* Return a relative path for destination relative to source
*
* @param string $source
* @param string $destination
* @return string
*/
function getRelativePath($source, $destination)
{
$ds = DIRECTORY_SEPARATOR;
// Process absolute paths only
if ($source{0} == $ds && $destination{0} == $ds) {
$pathEqualPos = 0;
for ($pos = 0, $len = strlen($source); $pos < $len; ++$pos) {
if ($source{$pos} != $destination{$pos}) {
break;
}
$source{$pos} == $ds and $pathEqualPos = $pos;
}
$subSource = substr($source, $pathEqualPos + 1);
$subDestination = substr($destination, $pathEqualPos + 1);
$sourceDepth = substr_count(rtrim($subSource, $ds), $ds);
if ($sourceDepth == 0) {
return '.' . $ds . $subDestination;
}
return str_repeat('..' . $ds, $sourceDepth) . $subDestination;
}
return $destination;
}
function isHttpUrl($url)
{
return substr($url, 0, 2) == '//' || ($prefix = substr($url, 0, 7)) == 'http://' || $prefix == 'https:/';
}
function migrationPath($path = null)
{
return $_SERVER['PHWOOLCON_MIGRATION_PATH'] . ($path ? '/' . $path : '');
}
function price($amount, $currency = 'CNY')
{
return I18n::formatPrice($amount, $currency);
}
/**
* @param string $dir
*/
function removeDir($dir)
{
if (is_dir($dir)) {
$files = scandir($dir);
foreach ($files as $file) {
if ($file != '.' && $file != '..') {
removeDir("$dir/$file");
}
}
rmdir($dir);
} elseif (file_exists($dir) || is_link($dir)) {
unlink($dir);
}
}
function secureUrl($path, array $queries = [])
{
return url($path, $queries, true);
}
/**
* Show execution trace for debugging
*
* @param bool $exit Set to true to exit after show trace.
* @param bool $print Set to true to print trace
*
* @return string
* @codeCoverageIgnore
*/
function showTrace($exit = true, $print = true)
{
$e = new Exception;
if ($print) {
echo '<pre>', $e->getTraceAsString(), '</pre>';
}
if ($exit) {
exit;
}
return $e->getTraceAsString();
}
/**
* Return sorted and merged result of a given array, which contains sort orders as top level keys.
* Values with smaller sort order will be overridden by bigger ones.
*
* Example:
* <code>
* $array = [
* 10 => [ // 10 is a sort order
* 'foo' => 'bar', // Holds value 'bar' in key 'foo'
* 'who' => 'me',
* ],
* 20 => [ // 20 is a bigger sort order
* 'foo' => 'baz', // This will override the key 'foo' with value 'baz'
* 'hello' => 'world', // New values will be merged
* ],
* ];
* var_export($result = arraySortedMerge($array));
* </code>
*
* Will produce:
* <code>
* $result = [
* 'foo' => 'baz',
* 'who' => 'me',
* 'hello' => 'world',
* ];
* </code>
*
* @param array $array
* @return array
*/
function arraySortedMerge(array $array)
{
ksort($array);
$mergedArray = [];
foreach ($array as $item) {
$mergedArray = array_merge($mergedArray, $item);
}
return $mergedArray;
}
function storagePath($path = null)
{
return $_SERVER['PHWOOLCON_ROOT_PATH'] . '/storage' . ($path ? '/' . $path : '');
}
/**
* @param string $path
* @return string
* @codeCoverageIgnore
*/
function publicPath($path = null)
{
return $_SERVER['PHWOOLCON_ROOT_PATH'] . '/public' . ($path ? '/' . $path : '');
}
/**
* Copy dir by symlink files, override destination files, if exists
*
* @param string $source
* @param string $destination
*/
function symlinkDirOverride($source, $destination)
{
if (is_dir($source)) {
is_dir($destination) or mkdir($destination, 0755, true);
$files = scandir($source);
foreach ($files as $file) {
if ($file != '.' && $file != '..') {
symlinkDirOverride("$source/$file", "$destination/$file");
}
}
} elseif (is_file($source)) {
is_file($destination) and unlink($destination);
symlinkRelative($source, $destination);
}
}
/**
* Creates a symlink with relative path to source
* On Windows, the file will be copied instead of symlink
*
* @param string $source
* @param string $destination
* @return bool
*/
function symlinkRelative($source, $destination)
{
if ($targetDir = dirname($destination)) {
is_dir($targetDir) or mkdir($targetDir, 0755, true);
}
// @codeCoverageIgnoreStart
if (Text::startsWith(PHP_OS, 'WIN')) {
return copy($source, $destination);
}
// @codeCoverageIgnoreEnd
$cwd = getcwd();
$relativePath = getRelativePath($destination, $source);
chdir(dirname($destination));
$result = symlink($relativePath, $destination);
chdir($cwd);
return (bool)$result;
}
/**
* @param string $path
* @param array $queries
* @param bool $secure
* @return string
*/
function url($path, array $queries = [], $secure = null)
{
// Store the variables which are almost unchanged between invocations as static
/* @var \Phalcon\Http\Request $request */
static $runningUnitTest, $config, $request, $baseDirs = [], $hostComponents;
$runningUnitTest === null and $runningUnitTest = Config::runningUnitTest();
$hostComponents === null and $hostComponents = parse_url(Config::get('app.url'));
$config === null || $runningUnitTest === true and $config = [
'enable_https' => Config::get('app.enable_https'),
'secure_routes' => Config::get('app.secure_routes'),
'host' => (isset($hostComponents['host']) ? $hostComponents['host'] :
'localhost') . (isset($hostComponents['port']) ? ':' . $hostComponents['port'] : ''),
];
$request === null and $request = Di::getDefault()['request'];
if (isHttpUrl($path)) {
return $path;
}
// Decide http or https
$path = trim($path, '/');
if ($config['enable_https']) {
Text::startsWith($path, 'admin', false) and $secure = true;
$secure === null && isset($config['secure_routes'][$path]) and $secure = $config['secure_routes'][$path];
// TODO Detect https via proxy
$secure === null and $secure = $request->getScheme() === 'https';
} else {
$secure = false;
}
$protocol = $secure ? 'https://' : 'http://';
// Decide host
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $config['host'];
// Decide base dir
$scriptName = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '';
if (isset($baseDirs[$scriptName])) {
$base = $baseDirs[$scriptName];
} else {
$base = $scriptName;
$base = trim(dirname($base), '/');
$base and $base .= '/';
$baseDirs[$scriptName] = $base;
}
$url = $protocol . $host . '/' . $base;
// Apply path and queries
$url .= $path;
if ($queries) {
$delimiter = strpos($path, '?') === false ? '?' : '&';
$url .= $delimiter . http_build_query($queries);
}
return $url;
}
// @codeCoverageIgnoreStart
// OPCache workaround
if (!function_exists('opcache_invalidate')) {
/**
* (PHP 5 &gt;= 5.5.0, PECL ZendOpcache &gt;= 7.0.0 )<br/>
* Invalidates a cached script
* @link http://www.php.net/manual/en/function.opcache-invalidate.php
* @param string $script <p>The path to the script being invalidated.</p>
* @param bool $force [optional] <p> If set to <b>TRUE</b>, the script will be invalidated
* regardless of whether invalidation is necessary.</p>
* @return boolean
* Returns <b>TRUE</b> if the opcode cache for <em>script</em> was
* invalidated or if there was nothing to invalidate, or <b>FALSE</b> if the opcode
* cache is disabled.
*/
function opcache_invalidate($script, $force = false)
{
return true;
}
}
if (!function_exists('opcache_reset')) {
/**
* (PHP 5 &gt;= 5.5.0, PECL ZendOpcache &gt;= 7.0.0 )<br/>
* Resets the contents of the opcode cache
* @link http://www.php.net/manual/en/function.opcache-reset.php
* @return boolean
* Returns <b>TRUE</b> if the opcode cache was reset, or <b>FALSE</b> if the opcode cache is disabled.
*/
function opcache_reset()
{
return true;
}
}
// Random Bytes workaround
if (!function_exists('random_bytes')) {
function random_bytes($length)
{
return openssl_random_pseudo_bytes($length);
}
}
// @codeCoverageIgnoreEnd
<?xml version="1.0"?>
<ruleset name="PHALCON-PSR2">
<description>The PSR-2 coding standard compatible with phalcon.</description>
<!-- Include the whole PSR-2 standard -->
<rule ref="PSR2">
<!-- Disable rule: Property name should not be prefixed with an underscore -->
<exclude name="PSR2.Classes.PropertyDeclaration.Underscore"/>
<!-- Disable rule: Method name should not be prefixed with an underscore -->
<exclude name="PSR2.Methods.MethodDeclaration.Underscore"/>
</rule>
<file>src</file>
<file>tests</file>
<exclude-pattern>src/Cli/Output/Stream.php</exclude-pattern>
<exclude-pattern>src/Protocol/StreamWrapperTrait.php</exclude-pattern>
<exclude-pattern>src/Protocol/StreamWrapperInterface.php</exclude-pattern>
<exclude-pattern>*.min.js</exclude-pattern>
<exclude-pattern>*.min.css</exclude-pattern>
<exclude-pattern>tests/root/vendor</exclude-pattern>
<exclude-pattern>tests/root/storage</exclude-pattern>
<arg value="sp"/>
<arg name="colors"/>
<arg name="report-width" value="180"/>
</ruleset>
<?xml version="1.0" encoding="UTF-8" ?>
<phpdoc>
<title>Phwoolcon API Reference</title>
<parser>
<target>ignore/phpdoc/docs-build</target>
<default-package-name>Phwoolcon</default-package-name>
</parser>
<transformer>
<target>docs</target>
</transformer>
<files>
<directory>src</directory>
<file>functions.php</file>
</files>
<transformations>
<template name="ignore/phpdoc/vendor/phwoolcon/phpdoc-markdown-public/data/templates/markdown-public"/>
</transformations>
</phpdoc>
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/phwoolcon/test-starter/start.php"
backupGlobals="false"
backupStaticAttributes="false"
verbose="true"
colors="true"
stderr="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="true"
>
<php>
<ini name="intl.default_locale" value="en"/>
<ini name="intl.error_level" value="0"/>
<ini name="memory_limit" value="-1"/>
</php>
<testsuites>
<testsuite name="Phwoolcon - Unit Tests">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Phwoolcon - Integration Tests">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
<file>functions.php</file>
<exclude>
<file>src/Cli/Command/Migrate/template.php</file>
<file>src/Protocol/StreamWrapperTrait.php</file>
<file>src/Cli/Output/Stream.php</file>
<file>src/Cli/Command/ClearCacheCommand.php</file>
<file>src/Cli/Command/PhpunitPickPackageCommand.php</file>
<!-- TODO Implement service tests -->
<file>src/Cli/Command/ServiceCommand.php</file>
</exclude>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="tests/root/storage/coverage" charset="UTF-8" yui="true" highlight="true"/>
</logging>
</phpunit>
<?php
namespace Phwoolcon;
use Phalcon\Di;
class Aliases
{
public static function register(Di $di)
{
if ($aliases = Config::get('app.class_aliases')) {
foreach ($aliases as $alias => $class) {
class_exists($alias, false) or class_alias($class, $alias);
}
}
}
}
<?php
namespace Phwoolcon\Assets\Resource;
use Phalcon\Assets\Resource\Css as PhalconCss;
use Phwoolcon\Assets\ResourceTrait;
class Css extends PhalconCss
{
use ResourceTrait;
}
<?php
namespace Phwoolcon\Assets\Resource;
use Phalcon\Assets\Resource\Js as PhalconJs;
use Phwoolcon\Assets\ResourceTrait;
class Js extends PhalconJs
{
use ResourceTrait;
}
<?php
namespace Phwoolcon\Assets;
use Exception;
use Phwoolcon\Log;
/**
* Class ResourceTrait
* @package Phwoolcon\Assets
*
* @property $_local
* @property $_path
*/
trait ResourceTrait
{
public static $basePath;
public static $runningUnitTest = false;
protected $content;
/**
* @param string $previousHash
* @return string
*/
public function concatenateHash($previousHash)
{
$hash = dechex(crc32($previousHash . $this->getContent(ResourceTrait::$basePath)));
if (ResourceTrait::$runningUnitTest) {
Log::debug(sprintf('Asset hash: [%s]', $hash));
}
return $hash;
}
public function getContent($basePath = null)
{
if ($this->content === null) {
if (ResourceTrait::$runningUnitTest) {
Log::debug(sprintf('Asset path: [%s]', $this->_path));
}
try {
if ($this->_local) {
$this->content = parent::getContent($basePath);
} else {
$path = $this->_path;
substr($this->_path, 0, 2) == '//' and $path = 'http:' . $path;
$this->content = file_get_contents($path, false, stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 1,
],
]));
}
} catch (Exception $e) {
Log::exception($e);
$this->content = '';
}
}
return $this->content;
}
public static function setBasePath($path)
{
ResourceTrait::$basePath = $path;
}
/**
* @param boolean $flag
*/
public static function setRunningUnitTests($flag)
{
ResourceTrait::$runningUnitTest = $flag;
}
}
<?php
namespace Phwoolcon;
use Phalcon\Di;
use Phalcon\Cache\Frontend\Data;
use Phalcon\Cache\Backend;
use Phwoolcon\Cache\Backend\Redis;
use Phwoolcon\Exception\InvalidConfigException;
/**
* Class Cache
* @package Phwoolcon
*
* @method static int decrement(string $keyName = null, int $value = null)
* @uses Redis::decrement()
* @method static bool exists(string $keyName = null)
* @uses Redis::exists()
* @method static int increment(string $keyName = null, int $value = null)
* @uses Redis::increment()
* @method static array queryKeys(string $prefix = null)
* @uses Redis::queryKeys()
*/
class Cache
{
const TTL_ONE_MINUTE = 60;
const TTL_TEN_MINUTES = 600;
const TTL_ONE_HOUR = 3600;
const TTL_ONE_DAY = 86400;
const TTL_ONE_WEEK = 604800;
const TTL_ONE_MONTH = 2592000;
/**
* @var Backend|Backend\File|Redis
*/
protected static $cache;
/**
* @var Di
*/
protected static $di;
public static function __callStatic($name, $arguments)
{
static::$cache === null and static::$cache = static::$di->getShared('cache');
return call_user_func_array([static::$cache, $name], $arguments);
}
/**
* @param string $key
* @return bool
*/
public static function delete($key)
{
static::$cache === null and static::$cache = static::$di->getShared('cache');
return static::$cache->delete($key);
}
/**
* @return bool
*/
public static function flush()
{
static::$cache === null and static::$cache = static::$di->getShared('cache');
return static::$cache->flush();
}
/**
* @param string $key
* @return mixed
*/
public static function get($key, $default = null)
{
static::$cache === null and static::$cache = static::$di->getShared('cache');
return (null === $value = static::$cache->get($key)) ? $default : $value;
}
public static function register(Di $di)
{
static::$di = $di;
$di->remove('cache');
static::$cache = null;
$di->setShared('cache', function () {
$frontend = new Data(['lifetime' => static::TTL_ONE_DAY]);
$default = Config::get('cache.default');
$config = Config::get('cache.drivers.' . $default);
$class = $config['adapter'];
$options = $config['options'];
// @codeCoverageIgnoreStart
if (!$class || !class_exists($class)) {
throw new InvalidConfigException("Invalid cache adapter {$class}, please check config file cache.php");
}
// @codeCoverageIgnoreEnd
isset($options['cacheDir']) and $options['cacheDir'] = storagePath($options['cacheDir']) . '/';
/* @var Backend $backend */
$backend = new $class($frontend, $options);
// @codeCoverageIgnoreStart
if (!$backend instanceof Backend) {
throw new InvalidConfigException("Cache adapter {$class} should extend " . Backend::class);
}
// @codeCoverageIgnoreEnd
return $backend;
});
}
/**
* @param string $key
*/
public static function set($key, $value, $ttl = null)
{
static::$cache === null and static::$cache = static::$di->getShared('cache');
static::$cache->save($key, $value, $ttl);
}
}
<?php
namespace Phwoolcon\Cache\Backend;
use Phalcon\Cache\Backend\Redis as PhalconRedis;
use Phalcon\Cache\Exception;
use Phalcon\Cache\FrontendInterface;
/**
* Class Redis
*
* @package Phwoolcon\Cache\Backend\Adapter
*
* @property FrontendInterface $_frontend
* @property \Redis $_redis
*/
class Redis extends PhalconRedis
{
protected $compressThreshold = 2048;
public function _connect()
{
$options = $this->_options;
$redis = new \Redis;
// @codeCoverageIgnoreStart
if (empty($options['host']) ||
empty($options['port']) ||
empty($options['persistent']) ||
empty($options['index'])
) {
throw new Exception('Unexpected inconsistency in options');
}
isset($options['compress_threshold']) and $this->compressThreshold = $options['compress_threshold'];
// @codeCoverageIgnoreEnd
$host = $options['host'];
$port = $options['port'];
$index = $options['index'];
if ($options['persistent']) {
$success = $redis->pconnect($host, $port, 0, $index);
} else // @codeCoverageIgnoreStart
{
$success = $redis->connect($host, $port);
}
// @codeCoverageIgnoreEnd
// @codeCoverageIgnoreStart
if (!$success) {
throw new Exception('Could not connect to the Redis server ' . $host . ':' . $port);
}
if (!empty($options['auth'])) {
$success = $redis->auth($options['auth']);
if (!$success) {
throw new Exception('Failed to authenticate with the Redis server');
}
}
// @codeCoverageIgnoreEnd
if ($index) {
$success = $redis->select($index);
// @codeCoverageIgnoreStart
if (!$success) {
throw new Exception('Redis server selected database failed');
}
// @codeCoverageIgnoreEnd
}
$this->_redis = $redis;
}
protected function afterRetrieve($content)
{
if (is_numeric($content)) {
return $content;
}
if (isset($content{1}) && $content{0} == 'g') {
// gb: binary gzip - since 17.7.5
if ($content{1} == 'b') {
$content = gzinflate(substr($content, 2));
} // @codeCoverageIgnoreStart
// gz: base64 encoded gzip - since 16.12.2
elseif ($content{1} == 'z') {
$content = gzinflate(base64_decode(substr($content, 2)));
}
// @codeCoverageIgnoreEnd
}
return $this->_frontend->afterRetrieve($content);
}
protected function beforeStore($content)
{
if (is_numeric($content)) {
return $content;
}
$content = $this->_frontend->beforeStore($content);
// Compress big content
if (strlen($content) > $this->compressThreshold) {
// gz: base64 encoded gzip - since 16.12.2
// $content = 'gz' . base64_encode(gzdeflate($content, 9));
// gb: binary gzip - since 17.7.5
$content = 'gb' . gzdeflate($content, 9);
}
return $content;
}
public function decrement($keyName = null, $value = null)
{
// @codeCoverageIgnoreStart
if ($keyName === null) {
$lastKey = $this->_lastKey;
} // @codeCoverageIgnoreEnd
else {
$lastKey = $this->_lastKey = $this->_prefix . $keyName;
}
// @codeCoverageIgnoreStart
if (!$redis = $this->_redis) {
$this->_connect();
$redis = $this->_redis;
}
// @codeCoverageIgnoreEnd
if (!$value) {
return $redis->decr($lastKey);
}
return $redis->decrBy($lastKey, $value);
}
public function delete($keyName)
{
// @codeCoverageIgnoreStart
if (!$redis = $this->_redis) {
$this->_connect();
$redis = $this->_redis;
}
// @codeCoverageIgnoreEnd
$lastKey = $this->_prefix . $keyName;
return (bool)$redis->del($lastKey);
}
public function exists($keyName = null, $lifetime = null)
{
// @codeCoverageIgnoreStart
if ($keyName === null) {
$lastKey = $this->_lastKey;
} // @codeCoverageIgnoreEnd
else {
$lastKey = $this->_prefix . $keyName;
}
if ($lastKey) {
// @codeCoverageIgnoreStart
if (!$redis = $this->_redis) {
$this->_connect();
$redis = $this->_redis;
}
// @codeCoverageIgnoreEnd
return $redis->exists($lastKey);
}
// @codeCoverageIgnoreStart
return false;
// @codeCoverageIgnoreEnd
}
public function flush()
{
// @codeCoverageIgnoreStart
if (!$redis = $this->_redis) {
$this->_connect();
$redis = $this->_redis;
}
// @codeCoverageIgnoreEnd
$redis->flushDB();
return true;
}
public function get($keyName, $lifetime = null)
{
// @codeCoverageIgnoreStart
if (!$redis = $this->_redis) {
$this->_connect();
$redis = $this->_redis;
}
// @codeCoverageIgnoreEnd
$lastKey = $this->_lastKey = $this->_prefix . $keyName;
$content = $redis->get($lastKey);
if ($content === false) {
return null;
}
return $this->afterRetrieve($content);
}
/**
* @return \Redis
* @codeCoverageIgnore
*/
public function getRedis()
{
return $this->_redis;
}
public function increment($keyName = null, $value = null)
{
// @codeCoverageIgnoreStart
if ($keyName === null) {
$lastKey = $this->_lastKey;
} // @codeCoverageIgnoreEnd
else {
$lastKey = $this->_lastKey = $this->_prefix . $keyName;
}
// @codeCoverageIgnoreStart
if (!$redis = $this->_redis) {
$this->_connect();
$redis = $this->_redis;
}
// @codeCoverageIgnoreEnd
if (!$value) {
return $redis->incr($lastKey);
}
return $redis->incrBy($lastKey, $value);
}
public function queryKeys($prefix = null)
{
// @codeCoverageIgnoreStart
if (!$redis = $this->_redis) {
$this->_connect();
$redis = $this->_redis;
}
// @codeCoverageIgnoreEnd
return $redis->keys($this->_prefix . $prefix . '*') ?: [];
}
public function save($keyName = null, $content = null, $lifetime = null, $stopBuffer = true)
{
// @codeCoverageIgnoreStart
if ($keyName === null) {
$lastKey = $this->_lastKey;
} // @codeCoverageIgnoreEnd
else {
$lastKey = $this->_lastKey = $this->_prefix . $keyName;
}
// @codeCoverageIgnoreStart
if (!$lastKey) {
throw new Exception('The cache must be started first');
}
// @codeCoverageIgnoreEnd
$frontend = $this->_frontend;
// @codeCoverageIgnoreStart
if (!$redis = $this->_redis) {
$this->_connect();
$redis = $this->_redis;
}
// @codeCoverageIgnoreEnd
$cachedContent = $content === null ? $frontend->getContent() : $content;
$preparedContent = $this->beforeStore($cachedContent);
if ($lifetime === null) {
$ttl = $this->_lastLifetime ?: $frontend->getLifetime();
} else {
$ttl = $lifetime;
}
$success = $redis->set($lastKey, $preparedContent, $ttl);
// @codeCoverageIgnoreStart
if (!$success) {
throw new Exception('Failed storing the data in redis');
}
// @codeCoverageIgnoreEnd
if ($stopBuffer === true) {
$frontend->stop();
}
// @codeCoverageIgnoreStart
if ($frontend->isBuffering()) {
echo $cachedContent;
}
// @codeCoverageIgnoreEnd
$this->_started = false;
return $success;
}
}
<?php
namespace Phwoolcon\Cache;
use ArrayIterator;
use Phwoolcon\Cache;
use Phwoolcon\Config;
use Phwoolcon\Db;
use Phwoolcon\Events;
use Phwoolcon\I18n;
use Phwoolcon\Router;
use Phwoolcon\View;
class Clearer
{
const TYPE_ASSETS = 'assets';
const TYPE_CONFIG = 'config';
const TYPE_LOCALE = 'locale';
const TYPE_META = 'meta';
const TYPE_ROUTES = 'routes';
const TYPE_LOADER = 'sloader';
const TYPE_DATABASE = 'db';
const TYPE_VOLT = 'pvolt';
protected static $types = [
self::TYPE_CONFIG => 'Clear config cache',
self::TYPE_META => 'Clear model metadata',
self::TYPE_LOCALE => 'Clear locale cache',
self::TYPE_ASSETS => 'Clear assets cache',
self::TYPE_ROUTES => 'Clear routes cache',
self::TYPE_LOADER => 'Clear loader cache',
self::TYPE_DATABASE => 'Clear database cache',
self::TYPE_VOLT => 'Clear volt cache',
];
public static function clear($types = null)
{
$types = array_flip((array)$types);
$clearAll = true;
$messages = new ArrayIterator();
// @codeCoverageIgnoreStart
if (isset($types['config'])) {
$clearAll = false;
Config::clearCache();
$messages[] = 'Config cache cleared.';
}
// @codeCoverageIgnoreEnd
if (isset($types['meta'])) {
$clearAll = false;
Db::clearMetadata();
$messages[] = 'Model metadata cleared.';
}
if (isset($types['locale'])) {
$clearAll = false;
I18n::clearCache();
$messages[] = 'Locale cache cleared.';
}
if (isset($types['assets'])) {
$clearAll = false;
View::clearAssetsCache();
$messages[] = 'Assets cache cleared.';
}
if (isset($types['routes'])) {
$clearAll = false;
Router::clearCache();
$messages[] = 'Routes cache cleared.';
}
if (isset($types['sloader'])) {
$clearAll = false;
self::clearLoaderCache();
$messages[] = 'Loader cache cleared.';
}
if (isset($types['db'])) {
$clearAll = false;
self::clearDatabaseCache();
$messages[] = 'Database cache cleared.';
}
if (isset($types['pvolt'])) {
$clearAll = false;
self::clearVoltCache();
$messages[] = 'Volt cache cleared.';
}
if ($clearAll) {
Cache::flush();
Config::clearCache();
Router::clearCache();
self::clearLoaderCache();
self::clearDatabaseCache();
self::clearVoltCache();
$messages[] = 'Cache cleared.';
}
Events::fire('cache:after_clear', $messages, $messages);
return $messages;
}
/**
* @return array
* @codeCoverageIgnore
*/
public static function getTypes()
{
return self::$types;
}
/**
* @param array $types
* @codeCoverageIgnore
*/
public static function setTypes($types)
{
self::$types = $types;
}
/**
* 清除加载缓存
* @author wangyu <wangyu@ledouya.com>
* @createTime 2018/5/3 14:58
*/
public static function clearLoaderCache()
{
if ($files = glob(storagePath('cache') . '/loader*')) {
foreach ($files as $file) {
unlink($file);
}
}
}
/**
* 清除数据库缓存
* @author wangyu <wangyu@ledouya.com>
* @createTime 2018/5/3 14:58
*/
public static function clearDatabaseCache()
{
if ($files = glob(storagePath('db') . '/*')) {
foreach ($files as $file) {
unlink($file);
}
}
}
/**
* 清除视图编译文件
* @author wangyu <wangyu@ledouya.com>
* @createTime 2018/5/5 18:06
*/
public static function clearVoltCache()
{
if ($files = glob(storagePath('volt') . '/*')) {
foreach ($files as $file) {
unlink($file);
}
}
}
}
<?php
namespace Phwoolcon;
use Phalcon\Di;
use Symfony\Component\Console\Application;
class Cli
{
protected static $consoleWidth;
public static function register(Di $di)
{
$app = new Application(Config::get('app.name'), Config::get('app.version'));
foreach (Config::get('commands') as $name => $class) {
// @codeCoverageIgnoreStart
if (!class_exists($class)) {
fwrite(STDERR, "[Warning] commands config: Class {$class} not found for {$name}" . PHP_EOL);
continue;
}
// @codeCoverageIgnoreEnd
$app->add(new $class($name, $di));
}
$_SERVER['SCRIPT_NAME'] = '/index.php';
return $app;
}
public static function getConsoleWidth()
{
if (!static::$consoleWidth) {
static::$consoleWidth = (int)`tput cols`;
static::$consoleWidth > 30 or static::$consoleWidth = 80;
}
return static::$consoleWidth;
}
}
<?php
namespace Phwoolcon\Cli;
use LogicException;
use Phalcon\Di;
use Phwoolcon\Cli;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
abstract class Command extends SymfonyCommand
{
protected $di;
/**
* @var \Symfony\Component\Console\Input\ArgvInput
*/
protected $input;
/**
* @var \Symfony\Component\Console\Output\ConsoleOutput
*/
protected $output;
/**
* @var SymfonyStyle
*/
protected $interactive;
protected $statusCode = 0;
protected $outputTimestamp = false;
public function __construct($name, Di $di)
{
$this->di = $di;
empty($_SERVER['PHWOOLCON_CLI_OUTPUT_TIMESTAMP']) or $this->outputTimestamp = true;
parent::__construct($name);
}
/**
* @codeCoverageIgnore
* @param string $question
* @param mixed $default
* @return string
*/
public function ask($question, $default = null)
{
return $this->interactive->ask($question, $default);
}
/**
* @codeCoverageIgnore
* @param string $question
* @param bool $default
* @return string
*/
public function confirm($question, $default = true)
{
return $this->interactive->confirm($question, $default);
}
public function createProgressBar($max = 0)
{
$progress = $this->interactive->createProgressBar($max);
$progress->setBarWidth(Cli::getConsoleWidth() - 12 - strlen($max) * 2);
return $progress;
}
public function execute(InputInterface $input, OutputInterface $output)
{
$this->input = $input;
$this->output = $output;
$this->interactive = new SymfonyStyle($input, $output);
$this->fire();
return $this->statusCode;
}
/**
* @codeCoverageIgnore
*/
public function fire()
{
throw new LogicException('You must override the fire() method in the concrete command class.');
}
/**
* @return Di
* @codeCoverageIgnore
*/
public function getDi()
{
return $this->di;
}
/**
* @param Di $di
* @return $this
* @codeCoverageIgnore
*/
public function setDi($di)
{
$this->di = $di;
return $this;
}
public function comment($message)
{
$this->writeln("<comment>{$message}</comment>");
}
/**
* @codeCoverageIgnore
* @param string $message
*/
public function debug($message)
{
$this->output->isDebug() and $this->comment($message);
}
/**
* @codeCoverageIgnore
* @param string $message
* @param int $statusCode
*/
public function error($message, $statusCode = 1)
{
$this->outputTimestamp and $message = $this->timestampMessage($message);
$output = $this->output instanceof ConsoleOutputInterface ? $this->output->getErrorOutput() : $this->output;
$output->writeln("<error>{$message}</error>");
$this->statusCode = $statusCode;
}
public function info($message)
{
$this->writeln("<info>{$message}</info>");
}
public function question($message)
{
$this->writeln("<question>{$message}</question>");
}
/**
* @codeCoverageIgnore
* @param string $message
*/
public function verbose($message)
{
$this->output->isVerbose() and $this->info($message);
}
/**
* @codeCoverageIgnore
* @param string $message
*/
public function veryVerbose($message)
{
$this->output->isVeryVerbose() and $this->info($message);
}
public function writeln($message)
{
$this->outputTimestamp and $message = $this->timestampMessage($message);
$this->output->writeln($message);
}
protected function timestampMessage($message)
{
return date('[Y-m-d H:i:s] ') . $message;
}
/**
* @codeCoverageIgnore
* @param $title
*/
protected function setCliTitle($title)
{
if (function_exists('cli_set_process_title')) {
cli_set_process_title($title);
}
}
}
<?php
namespace Phwoolcon\Cli\Command;
use Phwoolcon\Cache\Clearer;
use Phwoolcon\Cli\Command;
use Symfony\Component\Console\Input\InputOption;
class ClearCacheCommand extends Command
{
protected function configure()
{
$definition = [];
$shotCuts = [];
foreach (Clearer::getTypes() as $type => $label) {
$shotCut = isset($shotCuts[$type{0}]) ? null : $type{0};
$shotCuts[$shotCut] = true;
$definition[] = new InputOption($type, $shotCut, InputOption::VALUE_NONE, $label);
}
$this->setDefinition($definition)->setDescription('Clears cache');
}
public function fire()
{
$types = array_keys(array_filter($this->input->getOptions()));
foreach (Clearer::clear($types) as $message) {
$this->info($message);
}
}
}
<?php
namespace Phwoolcon\Cli\Command;
use Exception;
use Phalcon\Db\Column;
use Phalcon\Di;
use Phwoolcon\Cli\Command;
use Phwoolcon\Config;
use Phwoolcon\Db;
use Phwoolcon\Log;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class Migrate extends Command
{
/**
* @var \Phalcon\Db\Adapter\Pdo|\Phwoolcon\Db\Adapter\Pdo\Mysql
*/
protected $db;
protected $table = 'migrations';
protected $sql = [];
protected $migrated = [];
protected $rawMigrated = [];
protected function checkMigrationsTable()
{
$db = $this->db;
$db->tableExists($this->table) or $db->createTable($this->table, null, [
'columns' => [
new Column('file', [
'type' => Column::TYPE_VARCHAR,
'size' => 255,
'notNull' => true,
'primary' => true,
]),
new Column('run_at', [
'type' => Column::TYPE_TIMESTAMP,
'notNull' => true,
'default' => 'CURRENT_TIMESTAMP',
]),
],
'options' => [
'TABLE_COLLATION' => 'utf8_unicode_ci',
],
]);
return $this;
}
public function cleanMigrationsTable()
{
// @codeCoverageIgnoreStart
if (!Config::runningUnitTest()) {
return;
}
// @codeCoverageIgnoreEnd
$this->db = Db::connection();
$this->db->dropTable($this->table);
$this->checkMigrationsTable();
}
protected function configure()
{
$this->setDescription('Run migration scripts.')
->setAliases(['migrate:up']);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$this->db = Db::connection();
parent::execute($input, $output);
}
public function fire()
{
$this->checkMigrationsTable();
$this->runMigration();
}
/**
* @codeCoverageIgnore
*/
public function getDefaultTableCharset()
{
return Db::getDefaultTableCharset();
}
protected function loadMigrated()
{
$db = $this->db;
isset($this->sql[$sqlKey = 'load_migrated']) or $this->sql[$sqlKey] = strtr('SELECT * FROM `table`', [
'`table`' => $db->escapeIdentifier($this->table),
]);
$this->rawMigrated = $this->db->fetchAll($this->sql[$sqlKey]);
foreach ($this->rawMigrated as $row) {
$this->migrated[$row['file']] = $row['run_at'];
}
return $this;
}
protected function logAndShowInfo($info, $newline = true)
{
Log::info($info);
$newline ? $this->info($info) : $this->output->write("<info>{$info}</info>");
}
protected function migrationExecuted($filename, $flag = null)
{
$db = $this->db;
if ($flag === true) {
$db->insertAsDict($this->table, [
'file' => $filename,
'run_at' => $this->migrated[$filename] = date('Y-m-d H:i:s'),
]);
return true;
}
if ($flag === false) {
$db->delete($this->table, strtr('`file` = ?', ['`file`' => $db->escapeIdentifier('file')]), [$filename]);
unset($this->migrated[$filename]);
return false;
}
$this->migrated or $this->loadMigrated();
return isset($this->migrated[$filename]);
}
protected function runMigration()
{
$db = $this->db;
$migrated = false;
foreach (glob(migrationPath('*.php')) as $file) {
$filename = basename($file);
if ($this->migrationExecuted($filename)) {
continue;
}
$migrated = true;
$this->logAndShowInfo(sprintf('Start migration "%s"...', $filename), false);
$db->begin();
try {
$migration = include $file;
if (isset($migration['up']) && is_callable($migration['up'])) {
call_user_func($migration['up'], $db, $this);
}
$this->migrationExecuted($filename, true);
$db->commit();
} // @codeCoverageIgnoreStart
catch (Exception $e) {
$db->rollback();
Log::exception($e);
$this->error(' [ BAD ] ');
$this->error($e->getMessage());
return;
}
// @codeCoverageIgnoreEnd
$this->logAndShowInfo(' [ OK ] ');
}
if ($migrated) {
Db::clearMetadata();
} // @codeCoverageIgnoreStart
else {
$this->info('Nothing to be migrated.');
}
// @codeCoverageIgnoreEnd
}
public function clearMigratedCache()
{
$this->migrated = [];
$this->rawMigrated = [];
return $this;
}
}
<?php
use Phalcon\Db\Adapter\Pdo as Adapter;
use Phalcon\Db\Column;
use Phalcon\Db\Index;
use Phalcon\Db\Reference;
use Phwoolcon\Cli\Command\Migrate;
return [
'up' => function (Adapter $db, Migrate $migrate) {
},
'down' => function (Adapter $db, Migrate $migrate) {
},
];
<?php
namespace Phwoolcon\Cli\Command;
use Phwoolcon\Cli\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class MigrateCreate extends Command
{
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, 'The name of the migration')
->addOption('choose-target', 'c', InputOption::VALUE_NONE, 'Choose target if multiple')
->setDescription('Create a new migration script.')
->setAliases(['migrate:make']);
}
public function fire()
{
$filename = date('Y-m-d-His-') . $this->input->getArgument('name') . '.php';
$path = migrationPath($filename);
// @codeCoverageIgnoreStart
if (is_file($migrationCandidates = $_SERVER['PHWOOLCON_ROOT_PATH'] . '/vendor/phwoolcon/migrations.php')) {
$candidates = include $migrationCandidates;
$chooseTarget = $this->input->getOption('choose-target');
if (!$chooseTarget &&
isset($candidates['selected']) &&
isset($candidates['candidates'][$candidates['selected']])
) {
$path = $candidates['candidates'][$candidates['selected']] . '/' . $filename;
} elseif (count($candidates['candidates']) == 1) {
$path = reset($candidates['candidates']) . '/' . $filename;
} elseif (count($candidates['candidates']) > 1) {
$targets = array_merge([''], array_keys($candidates['candidates']));
unset($targets[0]);
$choose = $this->interactive->choice('Please choose migration target: ', $targets, reset($targets));
$candidates['selected'] = $choose;
fileSaveArray($migrationCandidates, $candidates);
$path = $candidates['candidates'][$choose] . '/' . $filename;
}
}
// @codeCoverageIgnoreEnd
file_put_contents($path, $this->template());
$this->output->writeln("<info>Created Migration:</info> {$filename}");
}
public function template()
{
return file_get_contents(__DIR__ . '/Migrate/template.php');
}
}
<?php
namespace Phwoolcon\Cli\Command;
use Exception;
use Phwoolcon\Db;
use Phwoolcon\Log;
use Symfony\Component\Console\Input\InputOption;
class MigrateList extends Migrate
{
protected function configure()
{
$this->setDescription('List available migrations.')
->addOption('installed', 'i', InputOption::VALUE_NONE, 'List installed migrations')
->addOption('all', 'a', InputOption::VALUE_NONE, 'List available and installed migrations');
}
public function fire()
{
$this->checkMigrationsTable();
if ($this->input->getOption('installed')) {
$this->listMigrated();
} elseif ($this->input->getOption('all')) {
$this->listMigrated();
$this->listToBeMigrated();
} else {
$this->listToBeMigrated();
}
}
protected function listMigrated()
{
$this->loadMigrated();
if ($this->migrated) {
$this->comment('Following migrations are installed:');
$this->interactive->table(['Script', 'Run at'], $this->rawMigrated);
} else {
$this->info('No migrations installed.');
}
return $this;
}
protected function listToBeMigrated()
{
$found = false;
foreach (glob(migrationPath('*.php')) as $file) {
$filename = basename($file);
if ($this->migrationExecuted($filename)) {
continue;
}
$found or $this->comment('Following migrations are ready for install:');
$found = true;
$this->info($filename);
}
$found or $this->info('Nothing to be migrated.');
return $this;
}
}
<?php
namespace Phwoolcon\Cli\Command;
use Exception;
use Phwoolcon\Config;
use Phwoolcon\Db;
use Phwoolcon\Log;
use Symfony\Component\Console\Input\InputOption;
class MigrateRevert extends Migrate
{
protected function configure()
{
$this->setDescription('Revert last migration.')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force execution without confirmation')
->setAliases(['migrate:down']);
}
public function fire()
{
$this->checkMigrationsTable();
if ($lastMigration = $this->getLastMigration()) {
$file = $lastMigration['file'];
$runAt = $lastMigration['run_at'];
$this->comment(sprintf(' You are going to revert migration "%s" which was run at %s', $file, $runAt));
if ($this->input->getOption('force') || $this->confirm('please confirm', false)) {
$this->revertMigration($file);
}
} // @codeCoverageIgnoreStart
else {
$this->info('No migrations to be reverted.');
}
// @codeCoverageIgnoreEnd
}
protected function getLastMigration()
{
$db = $this->db;
isset($this->sql[$sqlKey = 'get_last_migration']) or $this->sql[$sqlKey] =
strtr('SELECT * FROM `table` ORDER BY `run_at` DESC LIMIT 1', [
'`table`' => $db->escapeIdentifier($this->table),
'`run_at`' => $db->escapeIdentifier('run_at'),
]);
return $db->fetchOne($this->sql[$sqlKey]);
}
protected function revertMigration($filename)
{
$db = $this->db;
$db->begin();
$file = migrationPath($filename);
try {
$this->logAndShowInfo(sprintf('Start reverting migration "%s"...', $filename), false);
$migration = include $file;
if (isset($migration['down']) && is_callable($migration['down'])) {
call_user_func($migration['down'], $db, $this);
}
$this->migrationExecuted($filename, false);
$db->commit();
Db::clearMetadata();
$this->logAndShowInfo(' [ OK ] ');
} // @codeCoverageIgnoreStart
catch (Exception $e) {
$db->rollback();
Log::exception($e);
$this->error(' [ BAD ] ');
$this->error($e->getMessage());
}
// @codeCoverageIgnoreEnd
}
}
<?php
namespace Phwoolcon\Cli\Command\Package;
use PharData;
use Phwoolcon\Cli\Command;
/**
* Command to create a Phwoolcon package
*
* @package Phwoolcon\Cli\Command\Package
* @codeCoverageIgnore
*/
class CreateCommand extends Command
{
protected function configure()
{
$this->setDescription('Create a phwoolcon package under vendor dir.');
}
public function fire()
{
// Clear working dir
if (is_dir($workingDir = storagePath('package-skeleton'))) {
removeDir($workingDir);
}
// Extract skeleton
mkdir($workingDir, 0777, true);
$phar = new PharData(__DIR__ . '/skeleton.tar');
$phar->extractTo($workingDir);
// Run prefill script
$workingDir .= '/skeleton';
chdir($workingDir);
putenv('PHWOOLCON_ROOT_PATH=' . $_SERVER['PHWOOLCON_ROOT_PATH']);
$process = proc_open('php prefill-phwoolcon.php', [STDIN, STDOUT, STDERR], $pipes);
proc_close($process);
if (is_file($workingDir . '/prefill-phwoolcon.php')) {
$this->error('Error occurred while running `prefill-phwoolcon.php`');
}
}
}
No preview for this file type
<?php
namespace Phwoolcon\Cli\Command;
use Phwoolcon\Cli\Command;
use Symfony\Component\Console\Input\InputOption;
class PhpunitPickPackageCommand extends Command
{
protected function configure()
{
$this->setDescription('Pick a phwoolcon package for phpunit testing.')
->addOption('clear', 'c', InputOption::VALUE_NONE, 'Clear chosen result');
}
public function fire()
{
$outputFile = storagePath('phpunit-chosen-package');
if ($this->input->getOption('clear')) {
is_file($outputFile) and unlink($outputFile);
return;
}
$packages = [''];
$packageFiles = detectPhwoolconPackageFiles();
$rootPathLength = strlen($_SERVER['PHWOOLCON_ROOT_PATH']) + 1;
foreach ($packageFiles as $packageFile) {
$packages[] = substr(dirname(dirname($packageFile)), $rootPathLength);
}
unset($packages[0]);
$firstOption = reset($packages);
$default = is_file($outputFile) ? file_get_contents($outputFile) : $firstOption;
in_array($default, $packages) or $default = $firstOption;
$package = $this->interactive->choice('Run phpunit for ... ', $packages, $default);
file_put_contents($outputFile, $package);
}
}
<?php
namespace Phwoolcon\Cli\Command;
use Exception;
use Phwoolcon\Cli\Command;
use Phwoolcon\Config;
use Phwoolcon\Log;
use Phwoolcon\Queue\Listener;
use Phwoolcon\Util\Timer;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class QueueConsumeCommand extends Command
{
protected $outputTimestamp = true;
protected function configure()
{
$this->setDescription('Consume from a given queue')
->addArgument('queue', InputArgument::OPTIONAL, 'The queue name to consume from, e.g. async_email_sending');
$optional = InputOption::VALUE_OPTIONAL;
$this->addOption('topic', null, $optional, 'The queue topic to consume from')
->addOption('delay', null, $optional, 'Seconds to delay failed jobs', 0)
->addOption('interval', 'i', $optional, 'Seconds between jobs execution', 0)
->addOption('memory', null, $optional, 'The memory limit in megabytes', 128)
->addOption('sleep', null, $optional, 'Seconds to sleep when no job is available', 3)
->addOption('ttl', null, $optional, 'Seconds to keep this consumer running', 3600)
->addOption('tries', null, $optional, 'Number of times to attempt a job before logging it failed', 0)
->addOption('type', null, $optional, 'work type :fire;reserve', 'reserve')
->addOption('tags', null, $optional);
}
public function fire()
{
// $type = $this->input->getOption('type');
// if($type && $type == 'reserve'){
// self::reserve();
// return;
// }
$this->output->writeln('');
$this->info('QueueConsumeCommand Handler');
$this->info('topic: ' . $this->input->getOption('topic'));
$this->output->writeln('');
$runningUnitTest = Config::runningUnitTest();
$queueName = $this->input->getArgument('queue');
$topic = $this->input->getOption('topic');
$tags = $this->input->getOption('tags');
$delay = $this->input->getOption('delay');
$interval = $this->input->getOption('interval') * 1e6;
$sleep = $this->input->getOption('sleep');
$ttl = $this->input->getOption('ttl');
$maxTries = $this->input->getOption('tries');
$memory = $this->input->getOption('memory');
$memoryLimit = $memory * 1024 * 1024;
$listener = $this->getListener();
// Timer::start();
$i = 0;
while (true) {
// $this->info('---handler---');
// Memory leak protect
if (++$i >= 100) {
if (memory_get_usage() > $memoryLimit) {
$runningUnitTest and $this->comment('Memory leak protection');
break;
}
$i = 0;
}
try {
$listener->pop($queueName, $topic, $delay, $sleep, $maxTries, $tags);
$interval and usleep($interval);
} catch (Exception $e) {
Log::exception($e);
$this->error($e->getMessage());
break;
}
// Exit when exceeds ttl, let process manager (e.g. supervisord) to start the listener again
/*if (Timer::stop() >= $ttl) {
break;
}*/
}
}
public function reserve()
{
$runningUnitTest = Config::runningUnitTest();
$queueName = $this->input->getArgument('queue');
$topic = $this->input->getOption('topic');
$tags = $this->input->getOption('tags');
$delay = $this->input->getOption('delay');
$interval = $this->input->getOption('interval') * 1e6;
$sleep = $this->input->getOption('sleep');
$ttl = $this->input->getOption('ttl');
$maxTries = $this->input->getOption('tries');
$memory = $this->input->getOption('memory');
$memoryLimit = $memory * 1024 * 1024;
$listener = $this->getListener();
Timer::start();
$i = 0;
while ($listener->reserve($queueName, $topic, $delay, $sleep, $maxTries, $tags)) {
$this->info('---handler---');
// Memory leak protect
try {
if (++$i >= 100) {
if (memory_get_usage() > $memoryLimit) {
$runningUnitTest and $this->comment('Memory leak protection');
break;
}
$i = 0;
}
$interval and usleep($interval);
} catch (Exception $e) {
Log::exception($e);
$this->error($e->getMessage());
break;
}
// Exit when exceeds ttl, let process manager (e.g. supervisord) to start the listener again
if (Timer::stop() >= $ttl) {
break;
}
}
}
/**
* @return Listener
*/
protected function getListener()
{
$di = $this->di;
$listenerClass = Listener::class;
$di->has($listenerClass) and $listenerClass = $di->getRaw($listenerClass);
return new $listenerClass;
}
}
<?php
/**
* Created by IntelliJ IDEA.
* User: yeran
* Date: 2018/5/12
* Time: 上午12:32
*/
namespace Phwoolcon\Cli\Command;
use Exception;
use Phwoolcon\Config;
use Phwoolcon\Log;
use Phwoolcon\Queue\Listener;
use Phwoolcon\Util\Timer;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Phwoolcon\Cli\Command;
class RocketQueueConsumeCommand extends Command
{
protected $outputTimestamp = true;
protected function configure()
{
$this->setDescription('Consume from a given rocketMQ')
->addArgument('queue', InputArgument::OPTIONAL, 'The queue name to consume from, e.g. async_email_sending');
$optional = InputOption::VALUE_OPTIONAL;
$this->addOption('topic', null, $optional, 'The queue topic to consume from')
->addOption('delay', null, $optional, 'Seconds to delay failed jobs', 0)
->addOption('interval', 'i', $optional, 'Seconds between jobs execution', 0)
->addOption('memory', null, $optional, 'The memory limit in megabytes', 128)
->addOption('sleep', null, $optional, 'Seconds to sleep when no job is available', 3)
->addOption('ttl', null, $optional, 'Seconds to keep this consumer running', 3600)
->addOption('tries', null, $optional, 'Number of times to attempt a job before logging it failed', 0)
->addOption('type', null, $optional, 'work type :fire;reserve', 'reserve');
}
public function fire()
{
$runningUnitTest = Config::runningUnitTest();
$queueName = $this->input->getArgument('queue');
$topic = $this->input->getOption('topic');
$delay = $this->input->getOption('delay');
$interval = $this->input->getOption('interval') * 1e6;
$sleep = $this->input->getOption('sleep');
$ttl = $this->input->getOption('ttl');
$maxTries = $this->input->getOption('tries');
$memory = $this->input->getOption('memory');
$memoryLimit = $memory * 1024 * 1024;
$listener = $this->getListener();
Timer::start();
$i = 0;
while (true) {
// $this->info('---handler---');
// Memory leak protect
if (++$i >= 100) {
if (memory_get_usage() > $memoryLimit) {
$runningUnitTest and $this->comment('Memory leak protection');
break;
}
$i = 0;
}
try {
$listener->pop($queueName, $topic, $delay, $sleep, $maxTries);
$interval and usleep($interval);
} catch (Exception $e) {
Log::exception($e);
$this->error($e->getMessage());
break;
}
// Exit when exceeds ttl, let process manager (e.g. supervisord) to start the listener again
if (Timer::stop() >= $ttl) {
break;
}
}
}
public function reserve(){
$runningUnitTest = Config::runningUnitTest();
$queueName = $this->input->getArgument('queue');
$topic = $this->input->getOption('topic');
$delay = $this->input->getOption('delay');
$interval = $this->input->getOption('interval') * 1e6;
$sleep = $this->input->getOption('sleep');
$ttl = $this->input->getOption('ttl');
$maxTries = $this->input->getOption('tries');
$memory = $this->input->getOption('memory');
$memoryLimit = $memory * 1024 * 1024;
$listener = $this->getListener();
Timer::start();
$i = 0;
while ($listener->reserve($queueName, $topic, $delay, $sleep, $maxTries)) {
$this->info('---handler---');
// Memory leak protect
try {
if (++$i >= 100) {
if (memory_get_usage() > $memoryLimit) {
$runningUnitTest and $this->comment('Memory leak protection');
break;
}
$i = 0;
}
$interval and usleep($interval);
} catch (Exception $e) {
Log::exception($e);
$this->error($e->getMessage());
break;
}
// Exit when exceeds ttl, let process manager (e.g. supervisord) to start the listener again
if (Timer::stop() >= $ttl) {
break;
}
}
}
/**
* @return Listener
*/
protected function getListener()
{
$di = $this->di;
$listenerClass = Listener::class;
$di->has($listenerClass) and $listenerClass = $di->getRaw($listenerClass);
return new $listenerClass;
}
}
\ No newline at end of file
<?php
namespace Phwoolcon\Cli\Command;
use Phalcon\Di;
use Phwoolcon\Cli\Command;
use Phwoolcon\Daemon\Service;
use Symfony\Component\Console\Input\InputArgument;
class ServiceCommand extends Command
{
protected static $service;
protected function configure()
{
$this->setDefinition([
new InputArgument(
'action',
InputArgument::OPTIONAL,
"Specifies the action to the service. Should be one of :
start | stop | restart | reload | status | install | uninstall"
),
])->setDescription('Phwoolcon High Performance Service');
}
public function fire()
{
Service::register($this->di);
/* @var Service $service */
$service = $this->di->getShared('service');
$service->setCliCommand($this);
// self::$service = $service;
$serviceName = $service->getName();
switch (strtolower($this->input->getArgument('action'))) {
case 'start':
$this->comment("Starting {$serviceName}...");
$service->start();
break;
case 'stop':
$this->comment("Stopping {$serviceName}...");
$service->stop();
break;
case 'restart':
$this->comment("Stopping {$serviceName}...");
$service->stop();
sleep(1);
$this->comment("Starting {$serviceName}...");
$service->start();
break;
case 'reload':
$this->comment('Run: bin/cli service reload-create-new-instance');
$this->comment('then bin/cli service reload-shut-old-instance');
break;
case 'reload-create-new-instance':
$this->comment("Starting new {$serviceName}...");
$service->shift()
->start();
break;
case 'reload-shut-old-instance':
// Ensure new service instance is up
$port = $service->choosePort();
$retry = 0;
$service->sendCommand('status', $port, $error);
while ($error && $retry < 20) {
usleep(5e5);
++$retry;
$service->sendCommand('status', $port, $error);
}
if ($error) {
$this->error('Service reload failed: ' . var_export($error, true));
break;
}
$this->comment("Stopping old {$serviceName}...");
$service->stop('old');
$this->info('Service reloaded.');
break;
case 'status':
$service->showStatus();
break;
case 'install':
$service->install();
break;
case 'uninstall':
case 'remove':
$service->uninstall();
break;
default:
$this->info($this->getDescription());
$this->comment('Usage:');
$this->info('bin/cli service [ start | stop | restart | reload | status | install | uninstall ]');
$this->output->writeln('');
}
}
}
<?php
/**
* 命令行处理任务
* @author: wangyu <wangyu@ledouya.com>
* @createTime: 2018/4/23 9:12
*/
namespace Phwoolcon\Cli\Command;
use Exception;
use Phwoolcon\Cli\Command;
use Phwoolcon\Config;
use Phwoolcon\Log;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class TaskCommand extends Command
{
protected $outputTimestamp = true;
protected function configure()
{
$this->setDescription('Command line execute task.');
$optional = InputOption::VALUE_OPTIONAL;
$this->addOption('name', null, $optional, 'The task processing class and processing function')
->addOption('options', null, $optional, 'Custom options, URL query string', '');
}
public function fire()
{
set_time_limit(0);
$name = $this->input->getOption('name');
$options = $this->input->getOption('options');
if (empty($name)) {
$this->info($this->getDescription());
$this->comment('Usage:');
$this->info('bin\cli task --name Lestore\Test\Services\Tasks\TestTask::test');
$this->output->writeln('');
return false;
}
$classArray = explode('::', $name, 2);
if (empty($classArray[0]) || empty($classArray[1]) || !class_exists($classArray[0]) || !method_exists($classArray[0], $classArray[1])) {
$this->output->writeln('');
$this->error('Processing class or processing function not exists');
$this->output->writeln('');
return false;
}
$parameters = [];
if ($options) {
parse_str($options, $parameters);
}
try {
$returnValue = call_user_func($name, $parameters);
if (is_array($returnValue)) {
$returnValue = json_encode($returnValue, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} elseif (is_object($returnValue)) {
$returnValue = json_encode($returnValue, JSON_FORCE_OBJECT);
}
$this->output->writeln('');
$this->info($returnValue);
$this->output->writeln('');
} catch (\Exception $e) {
$this->output->writeln('');
$this->info($e->getMessage());
$this->output->writeln('');
}
}
}
<?php
namespace Phwoolcon\Cli\Output;
use Phwoolcon\Protocol\StreamWrapperInterface;
use Phwoolcon\Protocol\StreamWrapperTrait;
class Stream implements StreamWrapperInterface
{
use StreamWrapperTrait {
stream_write as streamWriteReturn;
}
protected static $output = [];
public function stream_flush()
{
}
public function stream_open($path, $mode, $options, &$openedPath)
{
$this->path = $path;
isset(static::$output[$path]) or static::$output[$path] = '';
return true;
}
public function stream_read($count)
{
$start = $this->cursor;
$eof = $this->eof;
$this->updateCursor($count);
return $eof ? false : substr(static::$output[$this->path], $start, $count);
}
public function stream_write($data)
{
// @codeCoverageIgnoreStart
if (!isset(static::$output[$this->path])) {
return 0;
}
// @codeCoverageIgnoreEnd
static::$output[$this->path] .= $data;
return $this->streamWriteReturn($data);
}
public function unlink($path)
{
unset(static::$output[$path]);
return true;
}
}
<?php
namespace Phwoolcon;
use Phalcon\Config as PhalconConfig;
use Phalcon\Di;
class Config
{
protected static $config;
protected static $preloadConfig = [];
public static function clearCache()
{
$environment = static::environment();
is_file($cacheFile = storagePath('cache/config-' . $environment . '.php')) and unlink($cacheFile);
Cache::delete('db_configs_' . $environment);
}
public static function environment()
{
return static::get('environment');
}
/**
* @param $key string
* @param $defaultValue mixed
* @return mixed
*/
public static function get($key = null, $defaultValue = null)
{
return $key === null ? static::$config : fnGet(static::$config, $key, $defaultValue, '.');
}
protected static function loadDb(PhalconConfig $config)
{
$dbConfig = new PhalconConfig(Model\Config::all());
$config->merge($dbConfig);
static::$config = $config->toArray();
}
protected static function loadFiles($files)
{
$settings = [];
foreach ($files as $file) {
// @codeCoverageIgnoreStart
if (!is_file($file)) {
continue;
}
// @codeCoverageIgnoreEnd
$key = pathinfo($file, PATHINFO_FILENAME);
$value = include $file;
$settings[$key] = is_array($value) ? $value : [];
}
return $settings;
}
public static function register(Di $di)
{
$environment = isset($_SERVER['PHWOOLCON_ENV']) ? $_SERVER['PHWOOLCON_ENV'] : 'production';
// @codeCoverageIgnoreStart
if (is_file($cacheFile = storagePath('cache/config-' . $environment . '.php'))) {
static::$config = include $cacheFile;
Config::get('app.cache_config') or static::clearCache();
return;
}
// @codeCoverageIgnoreEnd
// Load preload files (for unit testing)
$config = new PhalconConfig(static::$preloadConfig);
// Load default configs
$defaultFiles = glob($_SERVER['PHWOOLCON_CONFIG_PATH'] . '/*.php');
$config->merge(new PhalconConfig(static::loadFiles($defaultFiles)));
// Load override configs
$overrideDirs = glob($_SERVER['PHWOOLCON_CONFIG_PATH'] . '/override-*/');
foreach ($overrideDirs as $overrideDir) {
$overrideFiles = glob($overrideDir . '*.php');
$overrideSettings = static::loadFiles($overrideFiles);
$overrideConfig = new PhalconConfig($overrideSettings);
$config->merge($overrideConfig);
}
// Load environment configs
$environmentFiles = glob($_SERVER['PHWOOLCON_CONFIG_PATH'] . '/' . $environment . '/*.php');
$environmentSettings = static::loadFiles($environmentFiles);
$environmentSettings['environment'] = $environment;
$environmentConfig = new PhalconConfig($environmentSettings);
$config->merge($environmentConfig);
$di->remove('config');
$di->setShared('config', $config);
static::$config = $config->toArray();
Config::get('database.default') and static::loadDb($config);
// @codeCoverageIgnoreStart
if (Config::get('app.cache_config')) {
is_dir($cacheDir = dirname($cacheFile)) or mkdir($cacheDir, 0777, true);
fileSaveArray($cacheFile, static::$config, function ($content) {
$replacement = <<<'EOF'
$_SERVER['PHWOOLCON_ROOT_PATH'] . '
EOF;
return str_replace("'{$_SERVER['PHWOOLCON_ROOT_PATH']}", $replacement, $content);
});
}
// @codeCoverageIgnoreEnd
}
/**
* @param string $key
* @param mixed $value
* @return mixed
*/
public static function set($key, $value)
{
array_set(static::$config, $key, $value, '.');
return $value;
}
public static function runningUnitTest()
{
return static::environment() == 'testing';
}
}
<?php
namespace Phwoolcon;
use core\base\Exception\ApiException;
use core\common\ErrorCode;
use core\service\security\AES;
use core\service\security\KeyConfig;
use core\service\security\Secret;
use Phalcon\Mvc\Controller as PhalconController;
use Phwoolcon\Controller\Api;
/**
* Class Controllers
* @package Phwoolcon
*
* @property \Phalcon\Http\Request $request
* @property Session|Session\AdapterTrait $session
* @property View $view
*/
abstract class Controller extends PhalconController
{
use Api;
const BROWSER_CACHE_ETAG = 'etag';
const BROWSER_CACHE_CONTENT = 'content';
protected $pageTitles = [];
public function onConstruct()
{
}
/**
* 获取请求参数
* @param mixed $name 参数名称,不传则为获取所有参数
* @param string $default 默认值
* @param mixed $filter 过滤函数,可以使用,分割的字符串以及正则表达式
* @param mixed $is_validate 是否验证,如果该值不为false,则会对参数进行校验,例如参数为空或不合法等,如果该值为字符串,则提示信息为该值
* @return array|mixed|null|string
* @throws ApiException
* @author wangyu <wangyu@ledouya.com>
* @createTime 2018/4/20 9:51
* @version 1.3.0
*/
public function params($name = null, $default = '', $filter = null, $is_validate = false)
{
if ($value = $this->request->get($name)) {
$input = $value;
if (isset($input['_url'])) {
unset($input['_url']);
}
}
if (empty($input)) {
$input_string = file_get_contents('php://input');
$input_data = json_decode($input_string, true);
if (is_array($input_data)) {
$input = $input_data;
} else {
$input = [];
}
if (true === Config::get('security.state.data')) {
$encrypt_string = null;
if (is_array($input) && isset($input[ENCRYPT_FIELD_NAME])) {
$encrypt_string = $input[ENCRYPT_FIELD_NAME];
unset($input[ENCRYPT_FIELD_NAME]);
}
if (!empty($encrypt_string)) {
$contents = AES::aes128Decrypt($encrypt_string, Config::get('security.secret_key.data') ?: KeyConfig::SECRET_KEY_AES_LESTORE, Config::get('security.secret_iv.data') ?: KeyConfig::SECRET_KEY_AES_LESTORE);
unset($encrypt_string);
if (!empty($contents)) {
if (is_json($contents)) {
$decrypt_data = json_decode($contents, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} elseif (is_xml($contents)) {
$decrypt_data = xml_decode($contents);
} else {
$decrypt_data = null;
}
if (!empty($decrypt_data) && is_array($decrypt_data)) {
$input = array_merge($input, $decrypt_data);
}
unset($contents, $decrypt_data);
}
}
}
}
if (empty($name)) { //获取全部变量
$data = $input;
$filters = isset($filter) ? $filter : 'htmlspecialchars';
if ($filters) {
if (is_string($filters)) {
$filters = explode(',', $filters);
}
foreach ($filters as $filter) {
$data = array_map_recursive($filter, $data); //参数过滤
}
}
} elseif ((is_array($input) && isset($input[$name])) || is_string($input)) { //取值操作
if (is_string($input)) {
$data = $input;
} else {
$data = $input[$name];
}
$filters = isset($filter) ? $filter : 'htmlspecialchars';
if ($filters) {
if (is_string($filters)) {
if (0 === strpos($filters, '/')) {
if (1 !== preg_match($filters, (string)$data)) { //支持正则验证
if (false !== $is_validate) {
self::idebug('[ApiException] 参数获取异常,参数名称:' . $name);
throw new ApiException((true !== $is_validate && null !== $is_validate) ? $is_validate : '参数值校验失败', ErrorCode::INVALID_PARAMETER);
}
return isset($default) ? $default : null;
}
} else {
$filters = explode(',', $filters);
}
} elseif (is_int($filters)) {
$filters = array($filters);
}
if (is_array($filters)) {
foreach ($filters as $filter) {
if (function_exists($filter)) {
$data = is_array($data) ? array_map_recursive($filter, $data) : $filter($data); //参数过滤
} else {
$data = filter_var($data, is_int($filter) ? $filter : filter_id($filter));
if (false === $data) {
if (false !== $is_validate) {
self::idebug('[ApiException] 参数获取异常,参数名称:' . $name);
throw new ApiException((true !== $is_validate && null !== $is_validate) ? $is_validate : '参数值校验失败', ErrorCode::INVALID_PARAMETER);
}
return isset($default) ? $default : null;
}
}
}
}
}
} else {
$params = $this->request->getHeader($name);
if($params){
return $params;
}
if (false !== $is_validate) {
self::idebug('[ApiException] 参数获取异常,参数名称:' . $name);
throw new ApiException((true !== $is_validate && null !== $is_validate) ? $is_validate : '参数值校验失败', ErrorCode::INVALID_PARAMETER);
}
$data = isset($default) ? $default : null; //变量默认值
}
is_array($data) && array_walk_recursive($data, 'secure_filter');
return $data;
}
public function addPageTitle($title)
{
$this->pageTitles[] = $title;
return $this;
}
public function getBrowserCache($pageId = null, $type = null)
{
$pageId or $pageId = $this->request->getURI();
$cacheKey = md5($pageId);
switch ($type) {
case (static::BROWSER_CACHE_ETAG):
return Cache::get('fpc-etag-' . $cacheKey);
case (static::BROWSER_CACHE_CONTENT):
return Cache::get('fpc-content-' . $cacheKey);
}
return [
'etag' => Cache::get('fpc-etag-' . $cacheKey),
'content' => Cache::get('fpc-content-' . $cacheKey),
];
}
public function getContentEtag(&$content)
{
return 'W/' . dechex(crc32($content));
}
/**
* Q: Why make the `php://input` encapsulation?
* A: I want to use it in service mode, which is impossible to pass data via `php://input` between processes.
* This is not exactly the same as the Phalcon's implementation.
* @see \Phalcon\Http\Request::getRawBody()
* @return string
* @codeCoverageIgnore
*/
protected function getRawPhpInput()
{
isset($_SERVER['RAW_PHP_INPUT']) or $_SERVER['RAW_PHP_INPUT'] = file_get_contents('php://input');
return $_SERVER['RAW_PHP_INPUT'];
}
public function initialize()
{
$this->pageTitles = [__(Config::get('view.title_suffix'))];
isset($this->view) and $this->view->reset();
}
/**
* `Controllers::render(string $path[, string $view[, array $params]])`
*
* Or use two-parameter invocation: @param string $path
* @param string|array $view
* @param array $params
* @return bool|\Phalcon\Mvc\View
* @throws \Exception
* @since v1.1.6
* `Controllers::render(string $path[, array $params])`
*
*/
public function render($path, $view = '', array $params = [])
{
$params['page_title'] = $this->pageTitles;
return $this->view->render($path, $view, $params);
}
/**
* 提示视图
* @param $notice
* @return bool|\Phalcon\Mvc\View
* @throws \Exception
*/
public function renderNotice($notice)
{
$this->view->setVars(['notice' => $notice]);
return $this->view->render('auth', 'notice');
}
public function setBrowserCache($pageId = null, $type = null, $ttl = Cache::TTL_ONE_WEEK)
{
$pageId or $pageId = $this->request->getURI();
$cacheKey = md5($pageId);
$content = $this->response->getContent();
$eTag = $this->getContentEtag($content);
switch ($type) {
case (static::BROWSER_CACHE_ETAG):
Cache::set('fpc-etag-' . $cacheKey, $eTag, $ttl);
break;
case (static::BROWSER_CACHE_CONTENT):
Cache::set('fpc-content-' . $cacheKey, $content, $ttl);
break;
default:
Cache::set('fpc-etag-' . $cacheKey, $eTag, $ttl);
Cache::set('fpc-content-' . $cacheKey, $content, $ttl);
break;
}
$this->setBrowserCacheHeaders($eTag, $ttl);
return $this;
}
public function setBrowserCacheHeaders($eTag, $ttl = Cache::TTL_ONE_WEEK)
{
$this->response->setHeader('Expires', gmdate(DateTime::RFC2616, time() + $ttl))
->setHeader('Cache-Control', 'public, max-age=' . $ttl)
->setHeader('Pragma', 'public')
->setHeader('Last-Modified', gmdate(DateTime::RFC2616))
->setEtag($eTag);
}
/**
* response json content
*
* @param array $array
* @param int $httpCode
* @param string $contentType
* @return \Phalcon\Http\Response
*/
protected function jsonReturn(array $array, $httpCode = 200, $contentType = 'application/json')
{
return $this->response->setHeader('Content-Type', $contentType)
->setStatusCode($httpCode)
->setJsonContent($array);
}
/**
* redirect url
*
* @param null $location
* @param int $statusCode
* @return \Phalcon\Http\Response
*/
protected function redirect($location = null, $statusCode = 302)
{
return $this->response->redirect(url($location), true, $statusCode);
}
/**
* Get input from request
*
* @param string $key
* @param mixed $defaultValue
* @return mixed
*/
protected function input($key = null, $defaultValue = null)
{
return is_null($key) ? $_REQUEST : fnGet($_REQUEST, $key, $defaultValue);
}
/**
* 调试日志
* @param mixed $message 信息
* @param string $type 类型
* @author wangyu <wangyu@ledouya.com>
* @createTime 2018/5/24 10:58
*/
private static function idebug($message, $type = 'DEBUG')
{
if (IS_DEBUG) {
debug($message, $type);
}
}
}
<?php
namespace Phwoolcon\Controller;
use Phwoolcon\Config;
use Phwoolcon\Controller;
use Phwoolcon\Session;
use Phwoolcon\View;
/**
* Class Admin
* @package Phwoolcon\Controller
*
* @property View $view
* @method Controller addPageTitle(string $title)
*/
trait Admin
{
public function initialize()
{
parent::initialize();
$this->addPageTitle(__(Config::get('view.admin.title_suffix')));
$this->view->setLayout(Config::get('view.admin.layout'));
$this->view->isAdmin(true);
}
}
<?php
namespace Phwoolcon\Controller\Admin;
use Phalcon\Validation\Exception as ValidationException;
use Phwoolcon\Config;
use Phwoolcon\Model\Config as ConfigModel;
trait ConfigTrait
{
protected function filterConfig($key, $data)
{
unset($data['_black_list'], $data['_white_list']);
// Process white list
if (is_array($allowedKeys = Config::get($key . '._white_list'))) {
$allowedData = [];
foreach ($allowedKeys as $key) {
array_set($allowedData, $key, fnGet($data, $key));
}
return $allowedData;
}
// Process black list
if (is_array($sensitiveKeys = Config::get($key . '._black_list'))) {
foreach ($sensitiveKeys as $key) {
array_forget($data, $key);
}
return $data;
}
throw new ValidationException("Config group '{$key}' is protected");
}
protected function getCurrentConfig($key)
{
$data = Config::get($key);
return $this->filterConfig($key, $data);
}
public static function getKeyLabel($key)
{
$labelKey = 'config_key_' . $key;
$label = __($labelKey);
return $label == $labelKey ? $key : $label;
}
protected function keyList()
{
$keys = [];
foreach (Config::get() as $key => $value) {
if (is_array(fnGet($value, '_black_list')) || is_array(fnGet($value, '_white_list'))) {
$keys[$key] = static::getKeyLabel($key);
}
}
ksort($keys);
return $keys;
}
protected function submitConfig($key, $data)
{
if ($data === '' || $data === null) {
$value = null;
} else {
if (is_string($data)) {
$data = json_decode($data, true);
if (json_last_error() != JSON_ERROR_NONE) {
throw new ValidationException(json_last_error_msg(), json_last_error());
}
}
$value = $this->filterConfig($key, $data);
}
ConfigModel::saveConfig($key, $value);
return $value;
}
}
<?php
namespace Phwoolcon\Controller;
use core\service\security\AES;
use core\service\security\KeyConfig;
use InvalidArgumentException;
use Phwoolcon\Config;
use phwoolcon\core\common\CodeMsg;
use Phwoolcon\ErrorCodes;
use Phwoolcon\Log;
use Phwoolcon\Router;
/**
* Class Api
* @package Phwoolcon\Controller
*
* @method \Phalcon\Http\Response jsonReturn(array $array, $httpCode = 200, $contentType = 'application/json')
*/
trait Api
{
protected $jsonApiContentType = 'application/vnd.api+json';
protected $jsonApiVersion = ['version' => '1.0'];
public function initialize()
{
Router::disableCsrfCheck();
Router::disableSession();
}
public function missingMethod()
{
Router::throw404Exception(json_encode([
'jsonapi' => $this->jsonApiVersion,
'errors' => [
[
'status' => 404,
'code' => 404,
'title' => '404 Not Found',
],
],
]), $this->jsonApiContentType);
}
/**
* Returns JSON API data
* @see http://jsonapi.org/format/#document-resource-objects
*
* @param array $data SHOULD contain `id` and `type`, MAY contain `attributes`, `relationships`, `links`
* @param array $meta
* @param array $extraData
* @param int $status
* @return \Phalcon\Http\Response
*/
public function jsonApiReturnData(array $data, array $meta = [], array $extraData = [], $status = 200)
{
$extraData['jsonapi'] = $this->jsonApiVersion;
$extraData['data'] = $data;
$meta and $extraData['meta'] = $meta;
return $this->jsonReturn($extraData, $status, $this->jsonApiContentType);
}
/**
* Returns JSON API error
* @see http://jsonapi.org/format/#errors
*
* @param array $errors Each error SHOULD contain `code` and `title`,
* MAY contain `id`, `status`, `links`, `detail`, `source`
* @param array $meta
* @param array $extraData
* @param int $status
* @return \Phalcon\Http\Response
*/
public function jsonApiReturnErrors(array $errors, array $meta = [], array $extraData = [], $status = 400)
{
foreach ($errors as &$error) {
isset($error['code']) and $error['code'] = (string)$error['code'];
}
unset($error);
$extraData['jsonapi'] = $this->jsonApiVersion;
$extraData['errors'] = $errors;
$meta and $extraData['meta'] = $meta;
return $this->jsonReturn($extraData, $status, $this->jsonApiContentType);
}
public function jsonApiReturnMeta(array $meta, array $extraData = [], $status = 200)
{
$extraData['jsonapi'] = $this->jsonApiVersion;
$extraData['meta'] = $meta;
return $this->jsonReturn($extraData, $status, $this->jsonApiContentType);
}
/**
* jsonApiReturn
* @param mixed $errorCode 错误码
* @param mixed $errorMsg 信息
* @param array $data 返回数据
* @param array $extraData 扩展数据
* @param int $status 状态码
* @param bool $is_encrypt 本次响应是否需要加密数据
* @return \Phalcon\Http\Response
* @author wangyu <wangyu@ledouya.com>
* @createTime 2018/5/25 14:45
*/
public function jsonApiReturn($errorCode, $errorMsg = null, array $data = null, array $extraData = [], $status = 200, $is_encrypt = true)
{
$extraData['jsonapi'] = $this->jsonApiVersion;
if (!empty($data)) {
if (true === $is_encrypt && true === Config::get('security.state.data') && !isset($data[ENCRYPT_FIELD_NAME])) {
$extraData['data'][ENCRYPT_FIELD_NAME] = AES::aes128Encrypt(json_encode($data), Config::get('security.secret_key.data'), Config::get('security.secret_iv.data'));
} else {
$extraData['data'] = $data;
}
}
// $extraData['error'] = $errorCode ?: 1;
$extraData['error'] = $errorCode;
if (!$errorMsg) {
$errorCode = ErrorCodes::getCodeMsg($errorCode);
$errorMsg = ($errorCode && count($errorCode) == 2) ? $errorCode[1] : '';
}
$extraData['error_reason'] = $errorMsg;
return $this->jsonReturn($extraData, $status, $this->jsonApiContentType);
}
}
<?php
namespace Phwoolcon;
use ReflectionProperty;
use Phalcon\Di;
use Phalcon\Events\Event;
use Phalcon\Http\Response\Cookies as PhalconCookies;
/**
* Class Cookies
* @package Phwoolcon
*
* @method static bool delete(string $name)
* @method static Http\Cookie get(string $name)
* @method static PhalconCookies reset()
*/
class Cookies
{
/**
* @var Di
*/
protected static $di;
/**
* @var PhalconCookies
*/
protected static $cookies;
/**
* @var ReflectionProperty
*/
protected static $cookiesReflection;
protected static $options;
public static function __callStatic($name, $arguments)
{
return call_user_func_array([static::$cookies, $name], $arguments);
}
public static function register(Di $di)
{
static::$di = $di;
$di->set('Phalcon\\Http\\Cookie', 'Phwoolcon\\Http\\Cookie');
static::$cookies = static::$di->getShared('cookies');
static::$cookies->reset();
static::$options = $options = Config::get('cookies');
static::$cookies->useEncryption($encrypt = $options['encrypt']);
$encrypt and static::$di->getShared('crypt')
->setKey($options['encrypt_key'])
->setPadding(Crypt::PADDING_ZERO);
/* @var \Phalcon\Http\Response $response */
if ($response = $di->getShared('response')) {
$response->setCookies(static::$cookies);
}
Events::attach('view:generatePhwoolconJsOptions', function (Event $event) {
$options = $event->getData() ?: [];
$options['cookies'] = [
'domain' => static::$options['domain'],
'path' => static::$options['path'],
];
$event->setData($options);
return $options;
});
}
/**
* @param string $name
* @param mixed $value
* @param int $expire
* @param string $path
* @param bool $secure
* @param string $domain
* @param bool $httpOnly
* @return PhalconCookies
*/
public static function set(
$name,
$value = null,
$expire = 0,
$path = null,
$secure = null,
$domain = null,
$httpOnly = null
) {
$options = static::$options;
$path === null and $path = $options['path'];
$domain === null and $domain = $options['domain'];
return static::$cookies->set($name, $value, $expire, $path, $secure, $domain, $httpOnly);
}
/**
* @return Http\Cookie[]
*/
public static function toArray()
{
if (static::$cookiesReflection === null) {
static::$cookiesReflection = new ReflectionProperty(static::$cookies, '_cookies');
static::$cookiesReflection->setAccessible(true);
}
return static::$cookiesReflection->getValue(static::$cookies);
}
}
<?php
namespace Phwoolcon;
use Phalcon\Crypt as PhalconCrypt;
class Crypt extends PhalconCrypt
{
protected static $opensslCipher = 'AES-256-CBC';
protected static $ivSize;
public static function opensslEncrypt($text, $key = null)
{
static::$ivSize or static::$ivSize = openssl_cipher_iv_length(static::$opensslCipher);
$iv = substr(base64_encode(random_bytes(static::$ivSize)), 0, static::$ivSize);
return $iv . openssl_encrypt($text, static::$opensslCipher, $key, 0, $iv);
}
public static function opensslDecrypt($text, $key = null)
{
static::$ivSize or static::$ivSize = openssl_cipher_iv_length(static::$opensslCipher);
$iv = substr($text, 0, static::$ivSize);
return openssl_decrypt(substr($text, static::$ivSize), static::$opensslCipher, $key, 0, $iv);
}
public static function reset()
{
static::$opensslCipher = 'AES-256-CBC';
static::$ivSize = openssl_cipher_iv_length(static::$opensslCipher);
}
}
<?php
namespace Phwoolcon\Daemon;
use Closure;
use core\base\Exception\ApiException;
use ErrorException;
use Phwoolcon\Db;
use Phwoolcon\Util\Timer;
use Swoole\Server as SwooleServer;
use Phalcon\Di;
use Phalcon\Events\Event;
use Phalcon\Http\Response;
use Phwoolcon\Cli\Command;
use Phwoolcon\Config;
use Phwoolcon\Cookies;
use Phwoolcon\Events;
use Phwoolcon\Router;
use Phwoolcon\Session;
use XHProfRuns_Default;
class Service
{
const OUTPUT_BUFFER_SIZE = 2097152;
const OUTPUT_CHUNK_SIZE = 1048576;
/**
* @var Di
*/
protected static $di;
/**
* @var Command
*/
protected $cliCommand;
protected $config;
protected $runDir = '/tmp/phwoolcon/';
protected $debug = false;
protected $profiler;
protected $profilerDir;
protected $initScript;
protected $name;
/**
* @var SwooleServer
*/
protected $swoole;
protected $swoolePort;
protected $availablePorts = [9502, 9503];
protected $sockFile;
protected $pid;
protected $managerPid;
protected $commandServer;
protected $environmentVariables = [];
/**
* @var ServiceAwareInterface[]
*/
protected $serviceAwareComponents = [];
protected $debugData = [];
public function __construct($config)
{
// @codeCoverageIgnoreStart
if (PHP_SAPI != 'cli') {
throw new ErrorException('Service is designed to be run in CLI mode only.');
}
if (!class_exists('Swoole\Server', false)) {
throw new ErrorException('PHP extension swoole not installed!');
}
// @codeCoverageIgnoreEnd
defined('PHWOOLCON_SERVICE_MODE') or define('PHWOOLCON_SERVICE_MODE', true);
foreach ($_SERVER as $k => $v) {
substr($k, 0, 10) == 'PHWOOLCON_' and $this->environmentVariables[$k] = $v;
}
isset($config['buffer_output_size']) or $config['buffer_output_size'] = static::OUTPUT_BUFFER_SIZE;
isset($config['chunk_output_size']) or $config['chunk_output_size'] = static::OUTPUT_CHUNK_SIZE;
$this->config = $config;
$this->runDir = $config['run_dir'];
$this->debug = $config['debug'];
substr($this->runDir, -1) == '/' or $this->runDir .= '/';
is_dir($this->runDir) or mkdir($this->runDir, 0777, true);
is_dir($this->profilerDir = storagePath('profiler')) or mkdir($this->profilerDir, 0777, true);
$this->initScript = $this->config['linux_init_script'];
$this->name = basename($this->initScript);
}
/**
* Choose a port in [9502, 9503] to serve
*
* @param bool $swap True to swap port, otherwise remain previous port
* @return integer Return previous port
*/
public function choosePort($swap = false)
{
$portSaved = is_file($portFile = $this->runDir . 'service-port.php');
$port = $previousPort = ($portSaved ? include($portFile) : reset($this->availablePorts));
if ($swap) {
$portSaved = false;
foreach ($this->availablePorts as $port) {
if ($port != $previousPort) {
break;
}
}
}
$this->swoolePort = $port;
$this->sockFile = $this->runDir . 'service-' . $port . '.sock';
$portSaved or file_put_contents($portFile, sprintf('<?php return %d;', $port));
return $previousPort;
}
/**
* @param string $type
* @param string $message
*/
protected function cliOutput($type, $message)
{
$this->cliCommand and $this->cliCommand->{$type}($message);
}
protected function getDebugInfo(SwooleServer $server)
{
return [
'service' => 1,
'mem' => round(memory_get_usage() / 1024 / 1024, 2) . 'M',
'worker-id' => $server->worker_id,
'status' => json_encode($server->stats()),
'cost-ms' => Timer::stop() * 1000,
] + $this->debugData;
}
/**
* @return string
* @codeCoverageIgnore
*/
public function getName()
{
return $this->name;
}
/**
* Get service info, including pid, manager pid and port
*
* @param string $instance Specify instance name (e.g. "current", "old").
* If not specified, return combined info of all instances.
* @return array An array containing service info
*/
protected function getServiceInfo($instance = null)
{
$file = $this->runDir . 'service-info.php';
$info = is_file($file) ? include($file) : [];
return $instance ? fnGet($info, $instance, []) : $info;
}
protected function initSwoole()
{
$this->choosePort();
$server = $this->swoole = new SwooleServer($this->sockFile, 0, SWOOLE_PROCESS, SWOOLE_UNIX_STREAM);
$config = array_merge([
/**
* Dispatch mode
* @see http://wiki.swoole.com/wiki/page/277.html
*/
'dispatch_mode' => 2,
'log_file' => storagePath('logs/service.log'),
/**
* Content detect protocol
* @see http://wiki.swoole.com/wiki/page/484.html
*/
'open_length_check' => true,
/**
* Max upload size
* @see http://wiki.swoole.com/wiki/page/301.html
*/
'package_max_length' => 83886080,
'package_length_type' => 'N',
'package_length_offset' => 0,
'package_body_offset' => 4,
], $this->config);
unset($config['run_dir'], $config['linux_init_script'], $config['debug'], $config['start_on_boot']);
unset($config['profiler']);
$server->set($config);
$enableProfiler = false;// $this->config['profiler'] && function_exists('xhprof_enable');
$server->on('Start', [$this, 'onStart']);
$server->on('Shutdown', [$this, 'onShutdown']);
$server->on('ManagerStart', [$this, 'onManagerStart']);
$server->on('WorkerStart', [$this, 'onWorkerStart']);
$server->on('Receive', [$this, $enableProfiler ? 'profileReceive' : 'onReceive']);
}
public function onManagerStart(SwooleServer $server)
{
@cli_set_process_title($this->name . ': manager process');
}
public function onReceive(SwooleServer $server, $fd, $fromId, $data)
{
if ($this->debug) {
$this->debugData = [];
Timer::start();
}
$length = unpack('N', $data)[1];
$data = unserialize(substr($data, -$length));
$_REQUEST = $_GET = $_POST = isset($data['request']) ? $data['request'] : [];
$_COOKIE = isset($data['cookies']) ? $data['cookies'] : [];
$_FILES = isset($data['files']) ? $data['files'] : [];
$_SERVER = isset($data['server']) ? $data['server'] : [];
foreach ($this->environmentVariables as $k => $v) {
$_SERVER[$k] = $v;
}
$this->reset();
ob_start();
try {
$response = Router::dispatch();
$headers = $this->parseResponseHeaders($response);
$body = $response->getContent();
} catch (\Exception $exception) {
$headers = ['status' => '200 OK', 'headers' => []];
$headers['headers'][] = 'HTTP/1.1 200 OK';
if ($exception instanceof ApiException) {
$headers['headers'] = array_merge($headers['headers'], $exception->getHeaders());
$body = $exception->getBody();
} else {
$body = $exception->getMessage();
}
}
$extraContent = ob_get_clean();
$result = serialize([
'headers' => $headers,
'body' => $body . $extraContent,
'meta' => $this->debug ? $this->getDebugInfo($server) : ['service' => 1],
]);
if (strlen($result) >= $this->config['buffer_output_size']) {
$chunks = str_split($result, $this->config['chunk_output_size'] - 10);
$rounds = count($chunks);
} else {
$chunks = [$result];
$rounds = 1;
}
$server->send($fd, pack('N', strlen($rounds)), $fromId);
$server->send($fd, $rounds, $fromId);
foreach ($chunks as $chunk) {
$server->send($fd, pack('N', strlen($chunk)), $fromId);
$server->send($fd, $chunk, $fromId);
}
}
public function onShutdown(SwooleServer $server)
{
swoole_event_del($this->commandServer);
}
/**
* Callback after service started
* @param SwooleServer $server
*/
public function onStart(SwooleServer $server)
{
@cli_set_process_title($this->name . ': master process/reactor threads; port=' . $this->swoolePort);
// Send SIGTERM to master pid to shutdown service
$this->pid = $server->master_pid;
// Send SIGUSR1 to master pid to reload workers
$this->managerPid = $server->manager_pid;
$this->startCommandHandler();
$this->updateServiceInfo('current', [
'pid' => $this->pid,
'manager_pid' => $this->managerPid,
'port' => $this->swoolePort,
]);
$this->cliOutput('info', "pid = {$this->pid}; port = {$this->swoolePort}");
$this->cliOutput('info', 'Service started.');
}
public function onWorkerStart(SwooleServer $server, $workerId)
{
Db::reconnect();
@cli_set_process_title($this->name . ': worker process ' . $workerId);
}
protected function parseResponseHeaders(Response $response)
{
$headers = ['status' => '', 'headers' => [], 'set_cookies' => []];
// status
$headers['status'] = 'HTTP/1.1 ' . $response->getHeaders()->get('Status');
// headers
foreach ($response->getHeaders()->toArray() as $name => $value) {
$headers['headers'][] = $name . ($value === null ? '' : ': ' . $value);
}
// cookies
foreach (Cookies::toArray() as $cookie) {
$headers['set_cookies'][] = [
$cookie->getName(),
$cookie->getResponseValue(),
$cookie->getExpiration(),
$cookie->getPath(),
$cookie->getDomain(),
$cookie->getSecure(),
$cookie->getHttpOnly(),
];
}
return $headers;
}
protected function prepareDebugObservers()
{
Events::attach('router:after_dispatch', function (Event $event) {
$this->debugData['session-id'] = json_encode(Session::getId());
$this->debugData['session-data'] = json_encode(isset($_SESSION) ? $_SESSION : null);
});
}
/**
* Resolve service aware components
* Each component will be reset before every request is handled
*/
protected function prepareServiceAwareComponents()
{
/* @var Di\Service $service */
foreach (static::$di->getServices() as $name => $service) {
if (!$service->isShared()) {
continue;
}
$component = static::$di->getShared($name);
if ($component instanceof ServiceAwareInterface) {
$this->serviceAwareComponents[$name] = $component;
}
}
// Listen for further components
Events::attach('di:afterServiceResolve', Closure::bind(function (Event $event) {
$data = $event->getData();
$name = $data['name'];
$component = $data['instance'];
if ($component instanceof ServiceAwareInterface) {
$this->serviceAwareComponents[$name] = $component;
}
}, $this));
}
/**
* @param string $command
* @return string Process result
*/
protected function processCommand($command)
{
$server = $this->swoole;
switch ($command) {
case 'status':
$labels = [
'start_time' => 'Service started at',
'connection_num' => 'Current connections',
'request_count' => 'Total requests',
];
$stats = $server->stats();
$result = "Service is running. PID: {$server->master_pid}, port: {$this->swoolePort}";
$result .= "\nSwoole version: " . swoole_version();
foreach ($labels as $k => $label) {
$v = $stats[$k];
$k == 'start_time' and $v = date('Y-n-j H:i:s (e)', $v);
$result .= "\n{$label}: {$v}";
}
$result .= "\nWorkers: {$this->config['worker_num']}";
break;
case 'connections':
$stats = $server->stats();
$result = $stats['connection_num'];
break;
// @codeCoverageIgnoreStart
default:
$result = 'Bad command';
// @codeCoverageIgnoreEnd
}
return $result;
}
/**
* @param SwooleServer $server
* @param $fd
* @param $fromId
* @param $data
* @codeCoverageIgnore
*/
public function profileReceive(SwooleServer $server, $fd, $fromId, $data)
{
xhprof_enable(0, [
'ignored_functions' => [
'call_user_func',
'call_user_func_array',
],
]);
$this->onReceive($server, $fd, $fromId, $data);
$microTime = explode(' ', microtime());
$pathInfo = strtr(Router::getCurrentUri(), ['/' => '|']);
$reportFile = $microTime[1] . '-' . substr($microTime[0], 2) . '-' . $_SERVER['REQUEST_METHOD'] . $pathInfo;
$this->profiler or $this->profiler = new XHProfRuns_Default($this->profilerDir);
$this->profiler->save_run(xhprof_disable(), 'service', $reportFile);
}
public static function register(Di $di)
{
static::$di = $di;
$di->remove('service');
$di->setShared('service', function () {
return new static(Config::get('service'));
});
}
public function reset()
{
foreach ($this->serviceAwareComponents as $component) {
$component->reset();
}
}
/**
* Send command to service command handler
*
* @param string $command
* @param integer $port
* @param $error
* @return string
*/
public function sendCommand($command, $port = null, &$error = null)
{
$port or $port = $this->choosePort();
$sockFile = $this->runDir . "command-{$port}.sock";
if (!$socket = @stream_socket_client('unix://' . $sockFile, $errNo, $errStr, 5)) {
$error = ['err' => $errNo, 'message' => $errStr];
return "$errStr ($errNo)";
}
$error = false;
fwrite($socket, $command, strlen($command));
$response = fread($socket, 8192);
fclose($socket);
return $response;
}
/**
* @param Command $command
* @return $this
* @codeCoverageIgnore
*/
public function setCliCommand(Command $command)
{
$this->cliCommand = $command;
return $this;
}
/**
* Mark running instance as old
*
* @return $this
*/
public function shift()
{
$this->updateServiceInfo('shift');
$this->choosePort(true);
return $this;
}
public function showStatus($port = null, $exit = true, &$error = null)
{
$response = $this->sendCommand('status', $port, $error);
$error ? $this->cliOutput('error', 'Service not started.') : $this->cliOutput('info', $response);
// @codeCoverageIgnoreStart
if ($exit) {
exit($error ? 3 : 0);
}
// @codeCoverageIgnoreEnd
return $response;
}
public function start($dryRun = false)
{
$port = $this->choosePort();
$this->sendCommand('status', $port, $error);
// @codeCoverageIgnoreStart
if (!$error) {
$this->cliOutput('error', 'Service already started');
return false;
}
// @codeCoverageIgnoreEnd
Db::reconnect();
$this->prepareServiceAwareComponents();
$this->debug and $this->prepareDebugObservers();
if (!$dryRun) {
$this->initSwoole();
$this->swoole->start();
}
return true;
}
protected function startCommandHandler()
{
$sockFile = $this->runDir . "command-{$this->swoolePort}.sock";
file_exists($sockFile) and unlink($sockFile);
ini_set('html_errors', 0);
if (!$this->commandServer = stream_socket_server('unix://' . $sockFile, $errNo, $errStr)) {
// @codeCoverageIgnoreStart
$this->cliOutput('error', "Command handler start failed: {$errStr} ({$errNo})");
} // @codeCoverageIgnoreEnd
else {
swoole_event_add($this->commandServer, function () {
$conn = stream_socket_accept($this->commandServer, 0);
swoole_event_add($conn, function ($conn) {
$command = fread($conn, 128);
$result = $this->processCommand($command);
swoole_event_write($conn, $result);
swoole_event_del($conn);
});
});
$this->cliOutput('info', 'Command handler started.');
}
}
/**
* Stop service
* @param string $instance Specify "current" or "old". "current" by default.
*/
public function stop($instance = 'current')
{
// Get (current or old) service info
if ($serviceInfo = $this->getServiceInfo($instance)) {
list($pid, $managerPid, $port) = array_values($serviceInfo);
// Get serving connection numbers
$connections = $this->sendCommand('connections', $port, $error);
// Wait while all connections are served
// @codeCoverageIgnoreStart
while (!$error && $connections > 0) {
usleep(5e5);
$connections = $this->sendCommand('connections', $port, $error);
}
// @codeCoverageIgnoreEnd
// Send TERM signal to master process to stop service
posix_kill($pid, SIGTERM);
// Kill master process in Mac OS
// @codeCoverageIgnoreStart
if (PHP_OS == 'Darwin') {
sleep(1);
posix_kill($pid, SIGKILL);
}
// @codeCoverageIgnoreEnd
}
$this->cliOutput('info', 'Service stopped.');
}
/**
* @param string $key
* @param mixed $data
* @return $this
*/
protected function updateServiceInfo($key, $data = null)
{
$info = $this->getServiceInfo();
if ($key == 'shift') {
if (isset($info['current'])) {
$info['old'] = $info['current'];
unset($info['current']);
}
} else {
$info[$key] = $data;
}
$file = $this->runDir . 'service-info.php';
fileSaveArray($file, $info);
// Reset opcache and stat cache
opcache_reset();
clearstatcache();
return $this;
}
}
<?php
namespace Phwoolcon\Daemon;
interface ServiceAwareInterface
{
public function reset();
}
<?php
namespace Phwoolcon;
use DateTime as PhpDateTime;
class DateTime extends PhpDateTime
{
const RFC2616 = 'D, d M Y H:i:s T';
const MYSQL_DATETIME = 'Y-m-d H:i:s';
const MYSQL_DATETIME_MICRO = 'Y-m-d H:i:s.u';
}
<?php
namespace Phwoolcon;
use Phalcon\Events\Event;
use Phwoolcon\Exception\InvalidConfigException;
use Phwoolcon\Model\MetaData\InCache;
use Phalcon\Di;
use Phalcon\Db as PhalconDb;
use Phalcon\Db\Adapter\Pdo as Adapter;
class Db extends PhalconDb
{
/**
* @var Di
*/
protected static $di;
/**
* @var static
*/
protected static $instance;
protected static $defaultTableCharset = [];
/**
* @var Adapter[]|Db\Adapter\Pdo\Mysql[]
*/
protected $connections = [];
protected $config;
public function __construct($config)
{
//PHP_SAPI == 'cli' ? $config['connections']['mysql']['persistent'] = true : $config['connections']['mysql']['persistent'] = false;
$this->config = $config;
$ormOptions = $config['orm_options'];
$ormOptions['distributed'] = $config['distributed'];
Model::setup($ormOptions);
if (fnGet($this->config, 'query_log')) {
Events::attach('db', function ($event, $connection) {
Log::debug('---db--event--');
$adapter = $event->getSource();
$binds = $adapter->getSqlVariables();
Log::debug(json_encode($adapter->getDescriptor()));
Log::debug($adapter->getRealSQLStatement() . '; binds = ' . var_export($binds, 1));
});
}
}
public static function clearMetadata()
{
/* @var InCache $metadata */
$metadata = static::$di->getShared('modelsMetadata');
$metadata->reset();
}
/**
* @param string $name
* @return string
*/
public static function getDefaultTableCharset($name = null)
{
static::$instance === null and static::$instance = static::$di->getShared('dbManager');
return self::$defaultTableCharset[$name ?: static::$instance->config['default']];
}
/**
* @param string $name
* @return Adapter|Db\Adapter\Pdo\Mysql
*/
protected function connect($name)
{
$name = static::$instance->config[$name] ?? $name;
$connection = $this->config['connections'][$name];
$class = $connection['adapter'];
isset($connection['charset']) and static::$defaultTableCharset[$name] = $connection['charset'];
if (isset($connection['default_table_charset'])) {
static::$defaultTableCharset[$name] = $connection['default_table_charset'];
}
unset($connection['adapter'], $connection['default_table_charset']);
// @codeCoverageIgnoreStart
if (!$class || !class_exists($class)) {
throw new InvalidConfigException("Invalid db adapter {$class}, please check config file database.php");
}
// @codeCoverageIgnoreEnd
$adapter = new $class($connection);
// @codeCoverageIgnoreStart
if (!$adapter instanceof Adapter) {
throw new InvalidConfigException("Db adapter {$class} should extend " . Adapter::class);
}
// @codeCoverageIgnoreEnd
return $adapter;
}
/**
* @param string $name
* @return Adapter|Db\Adapter\Pdo\Mysql
* @throws PhalconDb\Exception
*/
public static function connection($name = null)
{
static::$instance === null and static::$instance = static::$di->getShared('dbManager');
$db = static::$instance;
// @codeCoverageIgnoreStart
if (!$name = $name ?: 'default') {
throw new PhalconDb\Exception('Please set default database connection in your production config!');
}
// @codeCoverageIgnoreEnd
if (!isset($db->connections[$name])) {
$db->connections[$name] = $adapter = $db->connect($name);
$adapter->setEventsManager(static::$di->getShared('eventsManager'));
}
return $db->connections[$name];
}
public static function reconnect($name = null)
{
static::$instance === null and static::$instance = static::$di->getShared('dbManager');
$db = static::$instance;
$db->disconnect($name = $name ? $db->config['name'] : $db->config['default']);
if (isset($db->connections[$name])) {
$db->connections[$name]->connect();
return $db->connections[$name];
}
// @codeCoverageIgnoreStart
return static::connection($name);
// @codeCoverageIgnoreEnd
}
public function disconnect($name)
{
if (isset($this->connections[$name])) {
$this->connections[$name]->close();
}
}
public static function register(Di $di)
{
static::$di = $di;
$di->remove('modelsMetadata');
$di->setShared('modelsMetadata', function () {
return new InCache();
});
$di->setShared('dbManager', function () {
return new static(Config::get('database'));
});
$di->setShared('db', function () {
return static::connection();
});
}
}
<?php
namespace Phwoolcon\Db\Adapter\Pdo;
use Phalcon\Db\Adapter\Pdo\Mysql as PhalconMysql;
class Mysql extends PhalconMysql
{
}
<?php
namespace Phwoolcon;
use Closure;
use Phalcon\Di;
use Phalcon\Events\Event;
class DiFix extends Di
{
/**
* Fix over clever di service resolver in phalcon 2.1.x:
* let definition = \Closure::bind(definition, dependencyInjector)
* which leads to php warning "Cannot bind an instance to a static closure"
*
* @param Di $di
* @codeCoverageIgnore
* @see https://github.com/phalcon/cphalcon/issues/11709
* @see https://github.com/phalcon/cphalcon/commit/d67bef6cd22ca35795681b61d1c331cdefa24b09#diff-aaf42492472386533320a8415b05c5a6
*/
public static function fix(Di $di)
{
if ($_SERVER['PHWOOLCON_PHALCON_VERSION'] > 2010000) {
$di->setInternalEventsManager($di->getShared('eventsManager'));
Events::attach('di:beforeServiceResolve', function (Event $event) {
/* @var Di $di */
$di = $event->getSource();
$data = $event->getData();
$name = $data['name'];
$parameters = $data['parameters'];
if (!isset($di->_services[$name])) {
return false;
}
/* @var Di\Service $service */
$service = $di->_services[$name];
if (!$service->isShared()) {
return false;
}
if (!(($definition = $service->getDefinition()) instanceof Closure)) {
return false;
}
return $parameters ? call_user_func_array($definition, $parameters) : call_user_func($definition);
});
}
}
}
<?php
namespace Phwoolcon;
use Phalcon\Di;
/**
* Class ErrorCodes
*
* Usage:
* 1. Define error codes in a locale file `error_codes.php`
* ```php
* <?php
* return [
* 'foo_error' => 'The foo error message: %param%',
* '1234' => 'Some error message for 1234',
* '2345_with_annotation' => 'Some numeric error code with annotation',
* ];
* ```
* 2. `bin/dump-autoload` to generate the IDE helper
* 3. Enjoy the magic methods:
* ```php
* <?php
*
* use ErrorCodes; // Use the alias instead of Phwoolcon\ErrorCodes;
*
* list($errorCode, $errorMessage) = ErrorCodes::getFooError('bar');
* var_dump($errorCode, $errorMessage); // prints 'foo_error' and 'The foo error message: bar'
*
* throw ErrorCodes::gen1234(RuntimeException::class); // This is identical to:
* // $errorMessage = 'Some error message for 1234';
* // $errorCode = 1234;
* // throw new RuntimeException($errorMessage, $errorCode)
*
* throw ErrorCodes::gen2345WithAnnotation(RuntimeException::class); // This is identical to:
* // $errorMessage = 'Some numeric error code with annotation';
* // $errorCode = 2345; // Annotation removed
* // throw new RuntimeException($errorMessage, $errorCode)
*
* throw ErrorCodes::genFooError(RuntimeException::class, 'bar'); // This is identical to:
* // $errorMessage = 'The foo error message: bar [foo_error]';
* // $errorCode = 0; // Error code in a exception must be a integer
* // throw new RuntimeException($errorMessage, $errorCode)
* ```
*
* @package Phwoolcon
*/
class ErrorCodes
{
/**
* @var Di
*/
protected static $di;
/**
* @var static
*/
protected static $instance;
public static function __callStatic($name, $arguments)
{
static::$instance or static::$instance = static::$di->getShared('error_codes');
if (Text::startsWith($name, 'get', false)) {
$errorCode = Text::uncamelize(substr($name, 3));
return call_user_func([static::$instance, 'getDetails'], $errorCode, $arguments);
}
if (Text::startsWith($name, 'gen', false)) {
$errorCode = Text::uncamelize(substr($name, 3));
$exception = array_shift($arguments);
return call_user_func([static::$instance, 'generateException'], $exception, $errorCode, $arguments);
}
// @codeCoverageIgnoreStart
return call_user_func_array([static::$instance, $name], $arguments);
// @codeCoverageIgnoreEnd
}
public static function getCodeMsg($errorCode,$arguments=[]){
static::$instance or static::$instance = static::$di->getShared('error_codes');
return call_user_func([static::$instance, 'getDetails'], $errorCode, $arguments);
}
protected static function detectPlaceholders($message)
{
$pattern = '/%([^%]*)%/';
preg_match_all($pattern, $message, $matches);
$placeholders = isset($matches[1]) ? $matches[1] : [];
return $placeholders;
}
/**
* @param string $locale
* @return array
*/
public static function getAllErrorCodes($locale = null)
{
// @codeCoverageIgnoreStart
if (!static::$di) {
return [];
}
// @codeCoverageIgnoreEnd
/* @var I18n $i18n */
$i18n = static::$di->getShared('i18n');
$locale or $locale = I18n::getCurrentLocale();
$locales = $i18n->loadLocale($locale);
return fnGet($locales, 'packages.error_codes', []);
}
public static function ideHelperGenerator()
{
$classContent = [];
foreach (static::getAllErrorCodes() as $code => $message) {
$name = Text::camelize((string)$code);
$parameters = array_map(function ($field) {
return '$' . lcfirst(Text::camelize((string)$field));
}, static::detectPlaceholders($message));
$exceptionCode = (int)$code;
$exceptionMessage = $message;
if ($exceptionCode > 0) {
$code = $exceptionCode;
} else {
$exceptionMessage .= ' [' . $code . ']';
}
$exceptionParameters = $parameters;
array_unshift($exceptionParameters, '$exception');
$parameters = implode(', ', $parameters);
$exceptionParameters = implode(', ', $exceptionParameters);
$classContent[] = <<<METHOD
public static function get{$name}({$parameters}) {
return ['{$code}', '{$message}'];
}
public static function gen{$name}({$exceptionParameters}) {
return new \$exception('{$exceptionMessage}', {$exceptionCode});
}
METHOD;
}
return implode(PHP_EOL . PHP_EOL, $classContent);
}
public static function register(Di $di)
{
static::$di = $di;
$di->remove('error_codes');
static::$instance = null;
$di->setShared('error_codes', function () {
return new static;
});
}
protected function generateException($exception, $errorCode, array $arguments)
{
$exceptionMessage = $this->getDetails($errorCode, $arguments)[1];
$exceptionCode = (int)$errorCode;
$exceptionCode > 0 or $exceptionMessage .= ' [' . $errorCode . ']';
return new $exception($exceptionMessage, $exceptionCode);
}
public function getDetails($errorCode, array $arguments)
{
$errorMessage = __($errorCode, null, $package = 'error_codes');
if ($arguments && count($arguments)) {
$params = [];
reset($arguments);
foreach ($this->detectPlaceholders($errorMessage) as $placeholder) {
$params[$placeholder] = current($arguments);
next($arguments);
if (key($arguments) === null) {
break;
}
}
$errorMessage = __($errorCode, $params, $package);
}
intval($errorCode) > 0 and $errorCode = intval($errorCode);
return [$errorCode, $errorMessage];
}
}
<?php
namespace Phwoolcon;
use Phalcon\Di;
use Phalcon\Events\Manager;
/**
* Class Events
* @package Phwoolcon
*
* @method static void detach(string $eventType, object $handler)
* @see Manager::detach()
* @method static void detachAll(string $type = null)
* @see Manager::detachAll()
*/
class Events
{
/**
* @var Di
*/
protected static $di;
/**
* @var Manager
*/
protected static $event;
public static function __callStatic($name, $arguments)
{
static::$event or static::$event = static::$di->getShared('eventsManager');
return call_user_func_array([static::$event, $name], $arguments);
}
public static function attach($eventType, $handler, $priority = 100)
{
static::$event or static::$event = static::$di->getShared('eventsManager');
static::$event->attach($eventType, $handler, $priority);
}
public static function fire($eventType, $source, $data = null, $cancelable = true)
{
static::$event or static::$event = static::$di->getShared('eventsManager');
return static::$event->fire($eventType, $source, $data, $cancelable);
}
public static function register(Di $di)
{
static::$di = $di;
}
}
<?php
namespace Phwoolcon\Exception\Http;
/**
* Throw this exception to terminate execution and response a 403 forbidden when CSRF verification failed
* @package Phwoolcon\Exception\Http
*/
class CsrfException extends ForbiddenException
{
}
<?php
namespace Phwoolcon\Exception\Http;
use Phwoolcon\Exception\HttpException;
/**
* Throw this exception to terminate execution and response a 403 forbidden
* @package Phwoolcon\Exception\Http
*/
class ForbiddenException extends HttpException
{
public function __construct($message, $headers = null)
{
parent::__construct($message, 403, $headers);
}
}
<?php
namespace Phwoolcon\Exception\Http;
use Phwoolcon\Exception\HttpException;
/**
* Throw this exception to terminate execution and response a 404 not found
* @package Phwoolcon\Exception\Http
*/
class NotFoundException extends HttpException
{
public function __construct($message, $headers = null)
{
parent::__construct($message, 404, $headers);
}
}
<?php
namespace Phwoolcon\Exception\Http;
use Phwoolcon\Exception\HttpException;
/**
* Throw this exception to terminate execution and response a 302 redirect
* @package Phwoolcon\Exception\Http
* @codeCoverageIgnore
*/
class RedirectException extends HttpException
{
public function __construct($path, $queries = [], $secure = null)
{
$headers = [
'Location' => url($path, $queries, $secure),
];
parent::__construct('Moved Temporarily', 302, $headers);
}
}
<?php
namespace Phwoolcon\Exception;
use RuntimeException;
class HttpClientException extends RuntimeException
{
}
<?php
namespace Phwoolcon\Exception;
use Phalcon\Http\Response;
use RuntimeException;
class HttpException extends RuntimeException
{
protected $headers = [];
public function __construct($message, $code, $headers = null)
{
parent::__construct($message, $code);
$headers and $this->headers = (array)$headers;
}
public function getHeaders()
{
return $this->headers;
}
/*public function toResponse()
{
$response = new Response($this->getMessage(), $this->getCode());
$headers = $this->getHeaders();
foreach ($headers as $name => $value) {
if (is_numeric($name)) {
list($name, $value) = explode(':', $value);
}
$response->setHeader(trim($name), trim($value));
}
return $response;
}*/
public function toResponse($contents = null, $headers = [])
{
$response = new Response();
$response->setStatusCode($this->getCode());
$headers = empty($headers) ? $this->igetHeaders() : $headers;
foreach ($headers as $name => $value) {
if (is_numeric($name)) {
list($name, $value) = explode(':', $value);
}
$response->setHeader(trim($name), trim($value));
}
$response->setContent(empty($contents) ? $this->getBody() : $contents); //设置响应内容
return $response;
}
public function igetHeaders()
{
return [
'content-type: application/vnd.api+json',
'exception-type: HttpException',
];
}
public function getBody()
{
return json_encode([
'jsonapi' => ['version' => '1.0'],
'error' => $this->getCode(),
'error_reason' => $this->getMessage()
]);
}
}
<?php
namespace Phwoolcon\Exception;
use InvalidArgumentException;
class InvalidConfigException extends InvalidArgumentException
{
}
<?php
namespace Phwoolcon\Exception;
use RuntimeException;
class QueueException extends RuntimeException
{
}
<?php
namespace Phwoolcon\Exception;
use LogicException;
class SessionException extends LogicException
{
}
<?php
/**
* Created by IntelliJ IDEA.
* User: yeran
* Date: 2018/1/22
* Time: 下午3:11
*/
namespace Phwoolcon\Exception;
use Phwoolcon\Log;
use RuntimeException;
use Throwable;
class SysException extends RuntimeException{
public function __construct($message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
Log::error('-----'.$message);
}
}
<?php
namespace Phwoolcon\Exception;
use UnexpectedValueException;
class WidgetException extends UnexpectedValueException
{
}
<?php
namespace Phwoolcon\Http;
use Phalcon\Di;
use Phwoolcon\Config;
use Phwoolcon\Exception\HttpClientException;
class Client
{
protected static $componentName = 'http-client';
/**
* @var Di
*/
protected static $di;
/**
* @var static
*/
protected static $instance;
protected $lastRequest;
/**
* @var string|null
*/
protected $lastResponse;
protected $lastResponseCode;
protected $lastResponseHeaders;
protected $lastCurlInfo;
protected $options = [
'connect_time_out' => 10,
'time_out' => 10,
'verify_ssl' => true,
// 'user_agent' => 'Phwoolcon HTTP Client; +https://github.com/phwoolcon',
/*
* 'host:port', e.g.: '127.0.0.1:8080'
*/
'proxy' => null,
/*
* Custom DNS: array of 'host:port:ip', e.g.:
* ['www.example.com:443:172.16.1.1', 'abc.example.com:80:172.16.1.1']
*/
'custom_dns' => [],
/*
* Additional cURL options
*/
'curl_options' => [],
];
public function __construct(array $options = [])
{
$this->options = array_replace($this->options, $options);
}
public static function register(Di $di)
{
static::$di = $di;
$di->remove(static::$componentName);
static::$instance = null;
$di->setShared(static::$componentName, function () {
return new static(Config::get(static::$componentName, []));
});
}
public static function getInstance()
{
static::$instance or static::$instance = static::$di->getShared(static::$componentName);
return static::$instance;
}
/**
* @param string $url
* @param array|string $data
* @param array $headers
* @return string|null
* @codeCoverageIgnore
*/
public static function delete($url, $data = '', $headers = [])
{
static::$instance or static::$instance = static::$di->getShared(static::$componentName);
return static::$instance->sendRequest($url, $data, 'DELETE', $headers);
}
/**
* @param string $url
* @param array|string $data
* @param array $headers
* @return string|null
*/
public static function get($url, $data = '', $headers = [])
{
static::$instance or static::$instance = static::$di->getShared(static::$componentName);
return static::$instance->sendRequest($url, $data, 'GET', $headers);
}
/**
* @param string $url
* @param array|string $data
* @param array $headers
* @return string|null
*/
public static function head($url, $data = '', $headers = [])
{
static::$instance or static::$instance = static::$di->getShared(static::$componentName);
return static::$instance->sendRequest($url, $data, 'HEAD', $headers);
}
/**
* @param string $url
* @param array|string $data
* @param array $headers
* @return string|null
*/
public static function post($url, $data = '', $headers = [])
{
static::$instance or static::$instance = static::$di->getShared(static::$componentName);
return static::$instance->sendRequest($url, $data, 'POST', $headers);
}
/**
* @param string $url
* @param array|string $data
* @param array $headers
* @return string|null
* @codeCoverageIgnore
*/
public static function put($url, $data = '', $headers = [])
{
static::$instance or static::$instance = static::$di->getShared(static::$componentName);
return static::$instance->sendRequest($url, $data, 'PUT', $headers);
}
/**
* @param string $url
* @param array|string $data
* @param string $method
* @param array $headers
* @return string|null
*/
public static function request($url, $data = '', $method = 'GET', $headers = [])
{
static::$instance or static::$instance = static::$di->getShared(static::$componentName);
return static::$instance->sendRequest($url, $data, $method, $headers);
}
/**
* @return array|null
*/
public function getLastRequest()
{
return $this->lastRequest;
}
/**
* @return string|null
*/
public function getLastResponse()
{
return $this->lastResponse;
}
/**
* @return int
*/
public function getLastResponseCode()
{
return $this->lastResponseCode;
}
/**
* @return array|null
*/
public function getLastCurlInfo()
{
return $this->lastCurlInfo;
}
/**
* @return array
*/
public function getLastResponseHeaders()
{
if (!is_array($this->lastResponseHeaders)) {
$headers = [];
foreach (explode("\r\n", $this->lastResponseHeaders) as $row) {
if (!$row) {
continue;
}
$parts = explode(':', $row, 2);
if (!isset($parts[1])) {
continue;
}
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
}
$this->lastResponseHeaders = $headers;
}
return $this->lastResponseHeaders;
}
/**
* @return array
*/
public function getOptions()
{
return $this->options;
}
/**
* @param array $options
* @return static
*/
public function setOptions(array $options)
{
$this->options = $options;
return $this;
}
/**
* @param string $url
* @param array|string $data
* @param string $method
* @param array $headers
* @return string
* @throws HttpClientException
*/
protected function sendRequest($url, $data = '', $method = 'GET', $headers = [])
{
$method = strtoupper($method);
$data = is_array($data) ? http_build_query($data) : (string)$data;
$hasData = isset($data{0});
$this->lastResponseCode = 0;
$this->lastResponse = $this->lastResponseHeaders = $this->lastCurlInfo = null;
$this->lastRequest = compact('url', 'data', 'method', 'headers');
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HEADER => 1,
CURLOPT_TIMEOUT => $this->options['time_out'],
CURLOPT_USERAGENT => $this->options['user_agent'],
CURLOPT_CONNECTTIMEOUT => $this->options['connect_time_out'],
CURLOPT_SSL_VERIFYPEER => $this->options['verify_ssl'],
CURLOPT_SSL_VERIFYHOST => $this->options['verify_ssl'] ? 2 : 0,
CURLOPT_HTTPHEADER => $headers,
]);
$this->options['custom_dns'] and curl_setopt($ch, CURLOPT_RESOLVE, $this->options['custom_dns']);
$this->options['proxy'] and curl_setopt($ch, CURLOPT_PROXY, $this->options['proxy']);
$this->options['curl_options'] and curl_setopt_array($ch, $this->options['curl_options']);
switch ($method) {
case 'HEAD':
curl_setopt($ch, CURLOPT_NOBODY, true);
// No break here
case 'GET':
$hasData and $url .= (strpos($url, '?') === false ? '?' : '&') . $data;
break;
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
$hasData and curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
break;
default:
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
$hasData and curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
break;
}
curl_setopt($ch, CURLOPT_URL, $url);
$response = curl_exec($ch);
$this->lastCurlInfo = $curlInfo = curl_getinfo($ch);
if ($errNo = curl_errno($ch)) {
$errMsg = 'cURL error (' . $errNo . '): "' . curl_error($ch) . '", request: ' .
var_export($this->lastRequest, true);
curl_close($ch);
throw new HttpClientException($errMsg, $errNo);
}
curl_close($ch);
$this->lastResponseCode = $curlInfo['http_code'];
$headerSize = $curlInfo['header_size'];
$this->lastResponseHeaders = substr($response, 0, $headerSize);
$this->lastResponse = substr($response, $headerSize);
return $this->lastResponse;
}
}
<?php
namespace Phwoolcon\Http;
use Phalcon\CryptInterface;
use Phalcon\Di;
use Phalcon\Http\Cookie as PhalconCookie;
use Phwoolcon\Cache;
use Phwoolcon\Cookies;
/**
* Class Cookie
* @package Phwoolcon\Http
*
* @property Di $_dependencyInjector
*/
class Cookie extends PhalconCookie
{
public function delete()
{
$this->_value = null;
Cookies::set(
$this->_name,
$this->_value,
time() - Cache::TTL_ONE_MONTH,
$this->_path,
$this->_secure,
$this->_domain,
$this->_httpOnly
)->get($this->_name)->useEncryption(false);
}
public function getResponseValue()
{
if ($this->_useEncryption && $this->_value) {
/* @var CryptInterface $crypt */
$crypt = $this->_dependencyInjector->getShared('crypt');
return $crypt->encryptBase64((string)$this->_value);
}
return $this->_value;
}
}
<?php
namespace Phwoolcon;
use Phalcon\Di;
use Phalcon\Events\Event;
use Phalcon\Translate\Adapter;
use Phwoolcon\Daemon\ServiceAwareInterface;
use Phwoolcon\Http\Cookie;
class I18n extends Adapter implements ServiceAwareInterface
{
/**
* @var Di
*/
protected static $di;
/**
* @var static
*/
protected static $instance;
protected $locale = [];
protected $localePath;
protected $defaultLocale;
protected $currentLocale;
protected $multiLocale = false;
protected $detectClientLocale = false;
protected $options = [];
protected $availableLocales = [];
/**
* @var \Phalcon\Http\Request
*/
protected $request;
protected $undefinedStrings = [];
protected $undefinedStringsLogFile;
public function __construct(array $options = [], $moduleOption = [])
{
parent::__construct($options);
$this->multiLocale = $options['multi_locale'];
$this->multiLocale and $this->availableLocales = $options['available_locales'];
$this->detectClientLocale = $options['detect_client_locale'];
$this->defaultLocale = $this->currentLocale = $options['default_locale'];
$this->localePath = $options['locale_path'];
$this->options = $options;
if (empty($moduleOption)) {
$this->loadLocale($this->defaultLocale);
} else {
$this->loadLocale($moduleOption['locale'], $moduleOption['force'], $moduleOption['files']);
}
$this->undefinedStringsLogFile = storagePath($options['undefined_strings_log']);
$this->loadUndefinedLocaleStrings();
$this->reset();
}
protected function _setLocale($locale)
{
if (strpos($locale, '..') || !is_dir($this->localePath . '/' . $locale)) {
$locale = $this->defaultLocale;
}
// Session::set('current_locale', $this->currentLocale = $locale);
$this->loadLocale($locale);
}
public static function checkMobile($mobile, $country = 'CN')
{
static::$instance or static::$instance = static::$di->getShared('i18n');
$pattern = fnGet(static::$instance->options, "verification_patterns.{$country}.mobile");
return $pattern ? (bool)preg_match($pattern, $mobile) : true;
}
public static function clearCache($memoryOnly = false)
{
static::$instance or static::$instance = static::$di->getShared('i18n');
static::$instance->locale = [];
if ($memoryOnly) {
return;
}
foreach (glob(static::$instance->localePath . '/*', GLOB_ONLYDIR) as $dir) {
$locale = basename($dir);
Cache::delete('locale.' . $locale);
}
is_file(static::$instance->undefinedStringsLogFile) and unlink(static::$instance->undefinedStringsLogFile);
static::$instance->undefinedStrings = [];
}
/**
* @codeCoverageIgnore
*/
public function exists($index)
{
return isset($this->locale[$this->currentLocale]['combined'][$index]);
}
public static function formatPrice($amount, $currency = 'CNY')
{
// TODO Use I18n config
$symbol = '¥';
$precision = 2;
$number = number_format($amount, $precision);
$sequence = 'symbolFirst';
return $sequence == 'symbolFirst' ? $symbol . $number : $number . $symbol;
}
public static function getAvailableLocales()
{
static::$instance or static::$instance = static::$di->getShared('i18n');
return static::$instance->availableLocales;
}
public static function getCurrentLocale()
{
static::$instance or static::$instance = static::$di->getShared('i18n');
//根据COOKIE中locale的值判断语言
if (true === static::$instance->options['detect_client_locale'] and !empty($_COOKIE['locale'])) {
static::$instance->currentLocale = $_COOKIE['locale'];
}
if (!in_array(static::$instance->currentLocale, array_flip(static::$instance->options['available_locales']))) {
static::$instance->currentLocale = static::$instance->options['default_locale'];
}
return static::$instance->currentLocale;
}
public function loadLocale($locale, $force = false, $moduleLocaleFiles = [])
{
if (isset($this->locale[$locale]) && !$force) {
return $this->locale[$locale];
}
$useCache = $this->options['cache_locale'];
$cacheKey = 'locale.' . $locale;
if ((empty($moduleLocaleFiles) || !IS_DEBUG) && $useCache && !$force && $cached = Cache::get($cacheKey)) {
$this->locale[$locale] = $cached;
return $this->locale[$locale];
}
$packages = [];
$combined = [];
$localeDir = $this->localePath . '/' . $this->currentLocale;
$localeFiles = array_merge(glob($localeDir . '/*.php'), glob($localeDir . '/*/*.php'), $moduleLocaleFiles);
foreach ($localeFiles as $file) {
$package = pathinfo($file, PATHINFO_FILENAME);
$package == 'error_code' and $package = 'error_codes';
$packageLocale = (array)include $file;
isset($packages[$package]) or $packages[$package] = [];
$packages[$package] = array_replace($packages[$package], $packageLocale);
$package == 'error_codes' or $combined = array_replace($combined, $packageLocale);
}
$this->locale[$locale] = compact('combined', 'packages');
$useCache and Cache::set($cacheKey, $this->locale[$locale]);
return $this->locale[$locale];
}
protected function loadUndefinedLocaleStrings()
{
is_file($file = $this->undefinedStringsLogFile) and $this->undefinedStrings = include $file;
return $this;
}
protected function logUndefinedLocaleStrings()
{
$file = $this->undefinedStringsLogFile;
fileSaveArray($file, $this->undefinedStrings);
}
public function query($string, $params = null, $package = null)
{
$locale = $this->currentLocale;
$package == 'error_code' and $package = 'error_codes';
if ($package && isset($this->locale[$locale]['packages'][$package][$string])) {
$translation = $this->locale[$locale]['packages'][$package][$string];
} elseif (isset($this->locale[$locale]['combined'][$string])) {
$translation = $this->locale[$locale]['combined'][$string];
} else {
if (!isset($this->undefinedStrings[$locale][$package][$string])) {
Log::debug("I18n: locale string not found: '{$string}'");
$this->loadUndefinedLocaleStrings();
// $this->undefinedStrings[$locale][$package][$string] = true;
$this->logUndefinedLocaleStrings();
}
$translation = $string;
}
return $params ? $this->replacePlaceholders($translation, $params) : $translation;
}
public static function register(Di $di)
{
static::$di = $di;
$di->setShared('i18n', function () {
return new static(Config::get('i18n'));
});
Events::attach('view:generatePhwoolconJsOptions', function (Event $event) {
$options = $event->getData() ?: [];
$options['locale'] = static::getCurrentLocale();
$event->setData($options);
return $options;
});
}
public function reset()
{
if (!$this->multiLocale) {
return;
}
$this->request or $this->request = static::$di->getShared('request');
if ($locale = $this->request->get('_locale')) {
$this->_setLocale($locale);
return;
}
if (($cookie = Cookies::get('locale')) && $locale = $cookie->useEncryption(false)->getValue()) {
$this->_setLocale($locale);
$cookie->setHttpOnly(false)
->delete();
return;
}
/*if ($locale = Session::get('current_locale')) {
$this->loadLocale($this->currentLocale = $locale);
return;
}*/
// @codeCoverageIgnoreStart
if ($this->detectClientLocale) {
if (empty($this->request->getBestLanguage())) {
// $locale = self::getCurrentLocale();
if (!empty($_COOKIE['locale'])) {
$locale = $_COOKIE['locale'];
if (!in_array($locale, array_flip($this->options['available_locales']))) {
$locale = $this->options['default_locale'];
}
} else {
$locale = $this->options['default_locale'];
}
} else {
$locale = $this->request->getBestLanguage();
}
$this->_setLocale($locale);
return;
}
// @codeCoverageIgnoreEnd
$this->loadLocale($this->currentLocale = $this->defaultLocale);
}
public static function staticReset()
{
static::$instance or static::$instance = static::$di->getShared('i18n');
static::$instance->reset();
}
public static function setLocale($locale)
{
static::$instance or static::$instance = static::$di->getShared('i18n');
static::$instance->_setLocale($locale);
}
public static function translate($string, array $params = null, $package = null)
{
static::$instance or static::$instance = static::$di->getShared('i18n');
return static::$instance->query($string, $params, $package);
}
public static function useMultiLocale($flag = null)
{
static::$instance or static::$instance = static::$di->getShared('i18n');
$flag === null or static::$instance->multiLocale = (bool)$flag;
return static::$instance->multiLocale;
}
}
<?php
namespace Phwoolcon;
use Phalcon\Di;
use Phalcon\Logger;
use Phalcon\Logger\Adapter\File;
use Phalcon\Logger\Formatter\Line;
use Phwoolcon\Exception\HttpException;
class Log extends Logger
{
/**
* @var \Phalcon\Logger\Adapter|File
*/
protected static $logger;
protected static $hostname;
/**
* @param string $message
* @param array $context
*/
public static function debug($message = null, array $context = [])
{
static::log(static::DEBUG, $message, $context);
}
/**
* @param string $message
* @param array $context
*/
public static function error($message = null, array $context = [])
{
static::log(static::ERROR, $message, $context);
}
/**
* @param \Throwable $e
*/
public static function exception($e)
{
$message = get_class($e);
$e instanceof HttpException or $message .= "\n" . $e->__toString();
static::error($message);
}
/**
* @param string $message
* @param array $context
*/
public static function info($message = null, array $context = [])
{
static::log(static::INFO, $message, $context);
}
/**
* @param string $type
* @param string $message
* @param array $context
*/
public static function log($type, $message = null, array $context = [])
{
static::$logger or static::$logger = Di::getDefault()->getShared('log');
$context['host'] = static::$hostname;
$context['request'] = fnGet($_SERVER, 'REQUEST_METHOD') . ' ' . fnGet($_SERVER, 'REQUEST_URI');
static::$logger->log($type, $message, $context);
}
public static function register(Di $di)
{
static::$hostname = gethostname();
$di->remove('log');
static::$logger = null;
$di->setShared('log', function () {
$filePath = storagePath('logs');
is_dir($filePath) or mkdir($filePath, 0777, true);
$filePath .= '/' . Config::get('app.log.file', 'phwoolcon.log');
$logger = new File($filePath);
$logger->setLogLevel(Config::get('app.log.level', Logger::NOTICE));
$formatter = $logger->getFormatter();
if ($formatter instanceof Line) {
$formatter->setDateFormat('Y-m-d H:i:s');
$formatter->setFormat('[%date%]{host}[%type%] {request} %message%');
}
return $logger;
});
}
/**
* @codeCoverageIgnore
* @param string $message
* @param array $context
*/
public static function warning($message = null, array $context = [])
{
static::log(static::WARNING, $message, $context);
}
}
<?php
namespace Phwoolcon;
use Phalcon\Di;
use Phwoolcon\Queue\Adapter\JobTrait;
use Swift;
use Swift_Attachment;
use Swift_Image;
use Swift_Mailer;
use Swift_Message;
use Swift_SendmailTransport;
use Swift_SmtpTransport;
use Swift_SwiftException;
class Mailer
{
const CONTENT_TYPE_HTML = 'text/html';
const CONTENT_TYPE_TEXT = 'text/plain';
/**
* @var Di
*/
protected static $di;
/**
* @var static
*/
protected static $instance;
protected static $mailerInitialized = false;
/**
* @var Swift_Mailer
*/
protected $mailerLibrary;
protected $async = false;
protected $enabled = false;
/**
* @var Queue\Adapter\DbQueue
*/
protected $queue;
protected $config;
protected $sender;
protected $logContent = true;
protected function __construct($config)
{
$this->config = $config;
$this->enabled = fnGet($config, 'enabled');
$this->logContent = fnGet($config, 'log_content');
$this->async = fnGet($config, 'async') && ($this->queue = Queue::connection('async_email_sending'));
$this->sender = [fnGet($config, 'sender.address') => fnGet($config, 'sender.name')];
switch (fnGet($config, 'driver')) {
case 'smtp':
$host = fnGet($config, 'smtp_host');
$port = fnGet($config, 'smtp_port');
$encryption = fnGet($config, 'smtp_encryption');
$username = fnGet($config, 'smtp_username');
$password = fnGet($config, 'smtp_password');
$transport = new Swift_SmtpTransport($host, $port, $encryption);
$transport->setUsername($username)->setPassword($password);
break;
// @codeCoverageIgnoreStart
case 'sendmail':
$transport = new Swift_SendmailTransport();
break;
default:
throw new Swift_SwiftException('Please specify available driver in mail config');
// @codeCoverageIgnoreEnd
}
$mailer = new Swift_Mailer($transport);
$this->mailerLibrary = $mailer;
}
/**
* @param array $definitions
* @return Swift_Attachment[]
*/
public function generateAttachments(array $definitions)
{
if (isset($definitions['data']) || isset($definitions['path'])) {
$definitions = [$definitions];
}
$attachments = [];
foreach ($definitions as $definition) {
// @codeCoverageIgnoreStart
if (is_string($definition)) {
$definition = ['path' => $definition];
}
// @codeCoverageIgnoreEnd
$filename = isset($definition['file_name']) ? $definition['file_name'] : null;
$contentType = isset($definition['file_type']) ? $definition['file_type'] : null;
if (isset($definition['data'])) {
$attachments[] = new Swift_Attachment($definition['data'], $filename, $contentType);
} elseif (isset($definition['path']) && is_file($definition['path'])) {
$attachment = Swift_Attachment::fromPath($definition['path'], $contentType);
$filename and $attachment->setFilename($filename);
$attachments[] = $attachment;
}
}
return $attachments;
}
/**
* @param JobTrait $job
* @param array $payload
*/
public static function handleQueueJob($job, array $payload)
{
static::$instance === null and static::$instance = static::$di->getShared('mailer');
call_user_func_array([static::$instance, 'realSend'], $payload);
}
protected function log($to, $subject, $body, $contentType, $cc)
{
$isHtml = $contentType == static::CONTENT_TYPE_HTML;
$fileExt = $isHtml ? '.html' : '.txt';
$firstTo = $to;
if (is_array($to)) {
foreach ($to as $email => $name) {
$firstTo = is_numeric($email) ? $name : $email;
break;
}
}
$dir = dirname($logFile = storagePath('mail/' . date('Ymd-His') . '.' . $firstTo . $fileExt));
is_dir($dir) or mkdir($dir, 0777, true);
$bodyText = is_array($body) ? $body['body'] : $body;
$additionalInfo = json_encode(
compact('to', 'subject', 'cc'),
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
$isHtml and $additionalInfo = "<pre>{$additionalInfo}</pre>";
file_put_contents($logFile, $bodyText . "\n\n" . $additionalInfo);
}
protected function queue($to, $subject, $body, $contentType = self::CONTENT_TYPE_TEXT, $cc = null)
{
$this->queue->push([static::class, 'handleQueueJob'], func_get_args());
}
public function realSend($to, $subject, $body, $contentType = self::CONTENT_TYPE_TEXT, $cc = null)
{
// Fetch body text
$bodyText = isset($body['body']) ? $body['body'] : $body;
$message = new Swift_Message($subject, $bodyText, $contentType);
// Process attachments
if (isset($body['attach'])) {
foreach ($this->generateAttachments((array)$body['attach']) as $attachment) {
$message->attach($attachment);
}
}
// Process inline medias
if (isset($body['embed']) && is_array($body['embed'])) {
$placeholders = [];
$replacements = [];
foreach ($body['embed'] as $placeholder => $embed) {
is_numeric($placeholder) and $placeholder = $embed;
$contentId = $message->embed(Swift_Image::fromPath($embed));
$placeholders[] = '%' . $placeholder . '%';
$replacements[] = $contentId;
}
$bodyText = str_replace($placeholders, $replacements, $bodyText);
$message->setBody($bodyText);
}
// Add receivers and sender
$message->setTo($to);
$cc and $message->setCc($cc);
$message->setFrom($this->sender);
// Send the mail
$sent = $this->mailerLibrary->send($message);
$this->mailerLibrary->getTransport()->stop();
return $sent;
}
public static function register(Di $di)
{
static::$di = $di;
$di->remove('mailer');
static::$instance = null;
if (!static::$mailerInitialized) {
static::$mailerInitialized = true;
Swift::autoload(Swift_Mailer::class);
}
$di->setShared('mailer', function () {
return new static(Config::get('mail'));
});
}
/**
* If `async` is disabled, the mail will be sent immediately (default behaviour)
* If `async` is enabled, the mail will be pushed into queue instead, then send it by a backend process:
* `bin/cli queue:consume async_email_sending`
*
* @param string|array $to Supports 'to@domain.com', ['to@domain.com'] or ['to@domain.com' => 'Display Name']
* @param string $subject
* @param string|array $body If you want to send attachment, or send embed media, please use array form:
* ['body' => 'Body text', 'attach' => $attachments, 'embed' => $embed]
* for the structure of $attachments, @see Mailer::generateAttachments()
* for the structure of $embed, @see Mailer::realSend()
* @param string $contentType
* @param string|array $cc @see $to
* @return int
*/
public static function send($to, $subject, $body, $contentType = self::CONTENT_TYPE_TEXT, $cc = null)
{
static::$instance === null and static::$instance = static::$di->getShared('mailer');
$mailer = static::$instance;
$mailer->logContent and $mailer->log($to, $subject, $body, $contentType, $cc);
if (!$mailer->enabled) {
return 0;
} elseif ($mailer->async && $mailer->queue) {
$mailer->queue($to, $subject, $body, $contentType, $cc);
return 1;
} else {
return $mailer->realSend($to, $subject, $body, $contentType, $cc);
}
}
}
<?php
namespace Phwoolcon;
use Phalcon\Db as PhalconDb;
use Phalcon\Db\AdapterInterface;
use Phalcon\Di;
use Phalcon\Mvc\Model as PhalconModel;
use Phwoolcon\Db\Adapter\Pdo\Mysql;
use Phwoolcon\Util\Counter;
/**
* Class Model
* @package Phwoolcon
*
* @property Di $_dependencyInjector
* @method PhalconModel\Message[] getMessages(string $filter = null)
* @method Mysql|PhalconDb\Adapter\Pdo getWriteConnection()
*/
abstract class Model extends PhalconModel
{
protected static $_distributedOptions = [
'node_id' => '001',
'start_time' => 1362931200,
];
protected static $_dataColumns = [];
protected static $_defaultValues = [];
protected $_additionalData = [];
protected $_jsonFields = [];
protected $_table;
protected $_pk = 'id';
protected $_isNew = true;
protected $_useDistributedId = true;
protected $_integerColumnTypes = [
PhalconDb\Column::TYPE_INTEGER => true,
PhalconDb\Column::TYPE_BIGINTEGER => true,
];
public function __call($method, $arguments)
{
if (($prefix = substr($method, 0, 3)) == 'get') {
$property = Text::uncamelize(substr($method, 3));
if ((null !== $result = $this->getData($property)) || $this->checkDataColumn($property)) {
return $result;
}
} elseif ($prefix == 'set') {
$property = Text::uncamelize(substr($method, 3));
return $this->setData($property, fnGet($arguments, 0));
}
// @codeCoverageIgnoreStart
return parent::__call($method, $arguments);
// @codeCoverageIgnoreEnd
}
protected function _exists(PhalconModel\MetaDataInterface $metaData, AdapterInterface $connection, $table = null)
{
if ($this->_isNew) {
return parent::_exists($metaData, $connection, $table);
}
// Be able to update primary keys
$primaryValues = [];
foreach ($metaData->getPrimaryKeyAttributes($this) as $field) {
$primaryValues[$field] = [$oldValue = fnGet($this->_snapshot, $field), $this->$field];
$this->$field = $oldValue;
}
$result = parent::_exists($metaData, $connection, $table);
foreach ($primaryValues as $field => $values) {
$this->$field = $values[1];
}
return $result;
}
/*protected function _postSave($success, $exists)
{
// @codeCoverageIgnoreStart
if (!$success) {
throw new PhalconModel\Exception($this->getStringMessages());
}
// @codeCoverageIgnoreEnd
return parent::_postSave($success, $exists);
}*/
/*protected function _preSave(PhalconModel\MetaDataInterface $metaData, $exists, $identityField)
{
// Phalcon prepareSave() Polyfill
// @codeCoverageIgnoreStart
if ($_SERVER['PHWOOLCON_PHALCON_VERSION'] < 2001100) {
$this->prepareSave();
}
// @codeCoverageIgnoreEnd
// Fix phalcon bug: attributeField . " is required" on empty values, it should detect null values instead
$emptyFields = [];
foreach ($this->defaultValues() as $k => $v) {
if (!$v) {
$emptyFields[$k] = $this->$k;
$this->$k = 1;
}
}
$result = parent::_preSave($metaData, $exists, $identityField);
foreach ($emptyFields as $k => $v) {
$this->$k = $v;
}
return $result;
}*/
public function addData(array $data)
{
$this->assign($data);
return $this;
}
public function afterFetch()
{
$this->_isNew = false;
foreach ($this->_jsonFields as $field) {
isset($this->$field) && is_string($data = $this->$field) and $this->$field = json_decode($data, true);
}
}
protected function afterSave()
{
$this->_isNew = false;
$this->setSnapshotData($this->toArray());
foreach ($this->_jsonFields as $field) {
isset($this->$field) && is_string($data = $this->$field) and $this->$field = json_decode($data, true);
}
}
/**
* @inheritdoc
* @codeCoverageIgnore
* @param string $fields
* @param string $referencedFields
* @param array $options
*/
protected function belongsTo($fields, $referenceModel, $referencedFields, $options = null)
{
return parent::belongsTo($fields, static::getInjectedClass($referenceModel), $referencedFields, $options);
}
public function checkDataColumn($column = null)
{
if (!isset(static::$_dataColumns[$key = get_class($this)])) {
static::$_dataColumns[$key] = $this->getModelsMetaData()->getDataTypes($this) ?: [];
}
return $column === null ? static::$_dataColumns[$key] : isset(static::$_dataColumns[$key][$column]);
}
public function clearData()
{
foreach ($this->checkDataColumn() as $attribute => $type) {
$this->$attribute = null;
}
$this->_additionalData = [];
return $this;
}
protected function defaultValues()
{
if (!isset(static::$_defaultValues[$key = get_class($this)])) {
$defaultValues = $this->getModelsMetaData()->getDefaultValues($this) ?: [];
foreach ($defaultValues as $k => $v) {
if ($v === null) {
unset($defaultValues[$k]);
}
}
static::$_defaultValues[$key] = $defaultValues;
}
return static::$_defaultValues[$key];
}
public function generateDistributedId()
{
$prefix = time() - static::$_distributedOptions['start_time'];
$suffix = Text::padOrTruncate(Counter::increment($this->_table), '0', 4);
$id = $prefix . static::$_distributedOptions['node_id'] . $suffix;
return $this->setId($id);
}
public function getAdditionalData($key = null)
{
return $key === null ? $this->_additionalData : fnGet($this->_additionalData, $key);
}
public function getData($key = null)
{
return $key === null ?
($this->_additionalData ? array_merge($this->toArray(), $this->_additionalData) : $this->toArray()) :
(isset($this->$key) ? $this->$key : null);
}
public function getId()
{
return isset($this->{$this->_pk}) ? $this->{$this->_pk} : null;
}
public function getInjectedClass($class)
{
return $this->_dependencyInjector->has($class) ? $this->_dependencyInjector->getRaw($class) : $class;
}
public function getStringMessages()
{
if (!$this->getMessages()) {
return '';
}
$messages = [];
foreach ($this->getMessages() as $message) {
$messages[] = $message->getMessage();
}
return implode('; ', $messages);
}
/**
* @inheritdoc
* @codeCoverageIgnore
*/
protected function hasMany($fields, $referenceModel, $referencedFields, $options = null)
{
return parent::hasMany($fields, static::getInjectedClass($referenceModel), $referencedFields, $options);
}
/**
* @inheritdoc
* @codeCoverageIgnore
*/
protected function hasManyToMany(
$fields,
$intermediateModel,
$intermediateFields,
$intermediateReferencedFields,
$referenceModel,
$referencedFields,
$options = null
) {
return parent::hasManyToMany(
$fields,
static::getInjectedClass($intermediateModel),
$intermediateFields,
$intermediateReferencedFields,
static::getInjectedClass($referenceModel),
$referencedFields,
$options
);
}
/**
* @inheritdoc
* @codeCoverageIgnore
* @param string $fields
* @param string $referencedFields
* @param array $options
*/
protected function hasOne($fields, $referenceModel, $referencedFields, $options = null)
{
return parent::hasOne($fields, static::getInjectedClass($referenceModel), $referencedFields, $options);
}
/**
* Runs once, only when the model instance is created at the first time
*/
public function initialize()
{
$this->_table and $this->setSource($this->_table);
/*$this->keepSnapshots(true);
$this->skipAttributes(array('created_at', 'updated_at'));
$this->useDynamicUpdate(true);*/
}
public function isNew()
{
return $this->_isNew;
}
/**
* Runs every time, when a model object is created
*/
protected function onConstruct()
{
$this->clearData();
}
/*protected function prepareSave()
{
// Serialize JSON fields
foreach ($this->_jsonFields as $field) {
isset($this->$field) && is_array($data = $this->$field) and $this->$field = json_encode($data);
}
// Process created_at and updated_at fields
$columns = $this->checkDataColumn();
// Generate distributed ID if not specified
if ($this->_useDistributedId && !$this->getId()) {
$this->generateDistributedId();
}
// Process default values on null fields
foreach ($this->defaultValues() as $k => $v) {
if ($this->$k === null) {
$this->$k = $v;
}
}
}*/
public function reset()
{
parent::reset();
$this->clearData();
$this->_isNew = true;
return $this;
}
public function setData($key, $value = null)
{
if (is_array($key)) {
return $this->clearData()
->addData($key);
}
$this->$key = $value;
$this->checkDataColumn($key) or $this->_additionalData[$key] = $value;
return $this;
}
public function setId($id)
{
$this->{$this->_pk} = $id;
return $this;
}
/**
* @codeCoverageIgnore
*/
public function setRelatedRecord($key, $value)
{
$this->_related[$key] = $value;
$this->_dirtyState = static::DIRTY_STATE_TRANSIENT;
return $this;
}
/*public static function setup(array $options)
{
PhalconModel::setup($options);
isset($options['distributed']) and static::$_distributedOptions = $options['distributed'];
}*/
/**
* @param $sql
* @param null $bind
* @return bool
*/
public function sqlExecute($sql, $bind = null)
{
$sql = $this->translatePhalconBindIntoPDO($sql, $bind);
return $this->getWriteConnection()->prepare($sql)->execute($bind);
}
/**
* @param $sql
* @param null $bind
* @return array
*/
public function sqlFetchAll($sql, $bind = null)
{
$sql = $this->translatePhalconBindIntoPDO($sql, $bind);
return $this->getReadConnection()->fetchAll($sql, PhalconDb::FETCH_ASSOC, $bind);
}
/**
* @param $sql
* @param null $bind
* @return array
*/
public function sqlFetchOne($sql, $bind = null)
{
$sql = $this->translatePhalconBindIntoPDO($sql, $bind);
return $this->getReadConnection()->fetchOne($sql, PhalconDb::FETCH_ASSOC, $bind);
}
/**
* @param $sql
* @param null $bind
* @return mixed
*/
public function sqlFetchColumn($sql, $bind = null)
{
$row = $this->sqlFetchOne($sql, $bind);
return $row ? reset($row) : $row;
}
protected function translatePhalconBindIntoPDO($sql, &$bind = null)
{
if (is_array($bind)) {
foreach ($bind as $key => $val) {
$replace = [":{$key}:" => ":{$key}"];
if (strstr($sql, ($from = "{{$key}:array}")) !== false) {
if (is_array($val)) {
$to = [];
foreach (array_values($val) as $vKey => $realVal) {
$bind[$to[] = ":{$key}_{$vKey}"] = $realVal;
}
$replace[$from] = implode(', ', $to);
unset($bind[$key]);
}
}
$sql = strtr($sql, $replace);
}
}
return $sql;
}
public function validation()
{
return !$this->validationHasFailed();
}
/**
* @param array|string $conditions
* @param array $bind
* @param string $order
* @param string $columns
* @return $this|false
*/
public static function findFirstSimple($conditions, $bind = [], $order = null, $columns = null)
{
$params = static::buildParams($conditions, $bind, $order, $columns);
return static::findFirst($params);
}
/**
* @param $conditions
* @param array $bind
* @param string $order
* @param string $columns
* @param string|int $limit
* @return \Phalcon\Mvc\Model\Resultset\Simple|\Phalcon\Mvc\Model\ResultsetInterface
*/
public static function findSimple($conditions = [], $bind = [], $order = null, $columns = null, $limit = null)
{
$params = static::buildParams($conditions, $bind, $order, $columns, $limit);
return static::find($params);
}
/**
* @param array $conditions
* @param array $bind
* @return mixed
*/
public static function countSimple($conditions = [], $bind = [])
{
$params = static::buildParams($conditions, $bind);
return static::count($params);
}
/**
* @param $conditions
* @param array $bind
* @param string $orderBy
* @param string $columns
* @param string|int $limit
* @return array
*/
public static function buildParams($conditions = [], $bind = [], $orderBy = null, $columns = null, $limit = null)
{
$params = [];
if (is_string($orderBy)) {
$params['order'] = $orderBy;
}
if ($columns) {
$params['columns'] = is_array($columns) ? implode(',', $columns) : $columns;
}
if (is_int($limit)) {
$params['limit'] = $limit;
} elseif (is_string($limit) && strpos($limit, ',')) {
list($limit, $offset) = explode(',', $limit);
$params['limit'] = (int)trim($limit);
$params['offset'] = (int)trim($offset);
}
// @codeCoverageIgnoreStart
if (empty($conditions)) {
return $params;
}
// @codeCoverageIgnoreEnd
if (empty($bind)) {
if (is_array($conditions)) {
$params['conditions'] = "";
$params['bind'] = [];
foreach ($conditions as $key => $value) {
if (!is_array($value)) {
$operator = '=';
$realValue = $value;
} else {
$operator = reset($value);
$realValue = next($value);
}
$bindKey = str_replace(['.'], '_', $key);
$column = isset($value['column']) ? $value['column'] : $key;
$params['conditions'] .= ($params['conditions'] == "" ? "" : " AND ") .
" {$column} {$operator} :{$bindKey}: ";
$params['bind'][$bindKey] = $realValue;
}
} else {
$params['conditions'] = $conditions;
}
} else {
$params['conditions'] = $conditions;
$params['bind'] = $bind;
}
// debug(json_encode($params));
return $params;
}
}
<?php
namespace Phwoolcon\Model;
use Phwoolcon\Cache;
use Phwoolcon\Config as PhwoolconConfig;
use Phwoolcon\Db;
use Phwoolcon\Model;
use Phalcon\Db\Column;
class Config extends Model
{
protected $_table = 'config';
protected $_useDistributedId = false;
protected $_jsonFields = ['value'];
public static function all()
{
$environment = PhwoolconConfig::environment();
if (null === $config = Cache::get($key = 'db_configs_' . $environment)) {
$db = Db::connection();
$db->tableExists('config') or static::createConfigTable();
$config = [];
/* @var static $row */
foreach (static::find() as $row) {
$config[$row->getData('key')] = $row->getData('value');
}
Cache::set($key, $config, Cache::TTL_ONE_MONTH);
}
return $config;
}
protected static function createConfigTable()
{
$db = Db::connection();
$db->createTable('config', null, [
'columns' => [
new Column('key', [
'type' => Column::TYPE_VARCHAR,
'size' => 32,
'notNull' => true,
'primary' => true,
]),
new Column('value', [
'type' => Column::TYPE_TEXT,
]),
],
]);
if (PhwoolconConfig::runningUnitTest()) {
static::saveConfig('_testing', ['k' => 'v']);
}
}
public static function saveConfig($key, $value)
{
if (false === strpos($key, '.')) {
if ($value === null) {
if ($config = static::findFirstSimple(['key' => $key])) {
$config->delete();
PhwoolconConfig::clearCache();
}
return;
}
} // Ability to save a sub key
else {
list($key, $subKeys) = explode('.', $key, 2);
$subValue = $value;
if ($existing = static::findFirstSimple(['key' => $key])) {
$value = $existing->getData('value');
} else {
$value = [];
}
array_set($value, $subKeys, $subValue);
}
/* @var Config $config */
$config = new static;
$config->setData('key', $key)
->setData('value', (array)$value)
->save();
PhwoolconConfig::clearCache();
}
}
<?php
namespace Phwoolcon\Model\DynamicTrait;
trait EmptyTrait
{
}
<?php
namespace Phwoolcon\Model\DynamicTrait;
use Phalcon\Di;
use Phwoolcon\Text;
class Loader
{
protected static $instance;
protected $suffix = 'ModelTrait';
protected function __construct()
{
spl_autoload_register([$this, 'autoLoad']);
}
public static function register(Di $di)
{
static::$instance === null and static::$instance = new static();
}
public function autoLoad($className)
{
if (Text::endsWith($className, $this->suffix, false) && trait_exists(EmptyTrait::class)) {
class_alias(EmptyTrait::class, $className);
return true;
}
// @codeCoverageIgnoreStart
return false;
// @codeCoverageIgnoreEnd
}
}
<?php
namespace Phwoolcon\Model\MetaData;
use Phwoolcon\Cache;
use Phalcon\Mvc\ModelInterface;
use Phalcon\Mvc\Model\MetaData;
use Phalcon\Mvc\Model\Exception;
/**
* Phwoolcon\Model\MetaData\InCache
*
* Stores model meta-data in cache.
*
*<code>
* $metaData = new \Phwoolcon\Model\Metadata\InCache();
*</code>
*/
class InCache extends MetaData
{
protected $_metaData = [];
protected $_cachedData = [];
public function getNonPrimaryKeyAttributes(ModelInterface $model)
{
return $this->getAttributes($model);
}
/**
* Reads meta-data from files
*
* @param string $key
* @return mixed
*/
public function read($key)
{
$this->_cachedData or $this->_cachedData = Cache::get('model-metadata');
return isset($this->_cachedData[$key]) ? $this->_cachedData[$key] : null;
}
/**
* Writes the meta-data to files
*
* @param string $key
* @param array $data
*/
public function write($key, $data)
{
$this->_cachedData[$key] = $data;
Cache::set('model-metadata', $this->_cachedData, Cache::TTL_ONE_MONTH);
}
public function reset()
{
parent::reset();
$this->_cachedData = [];
Cache::delete('model-metadata');
}
}
<?php
namespace Phwoolcon\Model;
use Phwoolcon\Model;
/**
* Class User
* @package Phwoolcon\Model
*
* @method string getEmail()
* @method string getMobile()
* @method string getUsername()
* @method UserProfile|false getUserProfile()
* @method $this setUsername(string $username)
* @method $this setUserProfile(UserProfile $profile)
*/
class User extends Model
{
protected $_table = 'users';
protected function assignUserProfile()
{
return $this->setUserProfile($this->_dependencyInjector->get(UserProfile::class));
}
public function getAvatar()
{
$avatar = $this->getUserProfile() ? $this->getUserProfile()->getAvatar() : '';
return $avatar ? url($avatar) : '';
}
public function getRememberToken()
{
return ($profile = $this->getUserProfile()) ? $profile->getData('remember_token') : null;
}
public function initialize()
{
$class = UserProfile::class;
$this->hasOne('id', $class, 'user_id', ['alias' => 'user_profile']);
parent::initialize();
}
protected function prepareSave()
{
if (!$profile = $this->getUserProfile()) {
$this->assignUserProfile();
}
parent::prepareSave();
}
public function removeRememberToken()
{
if ($this->getRememberToken()) {
$profile = $this->getUserProfile();
$profile->setData('remember_token', null);
$profile->save();
}
return $this;
}
public function setRememberToken($rememberToken)
{
if ($profile = $this->getUserProfile()) {
$profile->setData('remember_token', $rememberToken);
$profile->save();
}
return $this;
}
}
<?php
namespace Phwoolcon\Model;
use Phwoolcon\Model;
use Phwoolcon\Text;
/**
* Class UserProfile
*
* @package Phwoolcon\Model
*
* @method string getAvatar()
* @method int getUserId()
*/
class UserProfile extends Model
{
protected $_table = 'user_profile';
protected $_pk = 'user_id';
protected $_jsonFields = ['extra_data'];
protected $extra_data = [];
public function generateAvatarUrl($id = null, $path = 'uploads/avatar')
{
$id === null and $id = $this->getId();
$subPath = substr(base62encode(crc32($id) + 62), 0, 2);
$fileName = base62encode($id);
$url = $path . '/' . $subPath . '/' . $fileName . '.png';
return $url;
}
public function generateResetPasswordToken()
{
$token = $this->getUserId() . '-' . Text::random();
$this->setExtraData('reset_password_token', $token);
$this->setExtraData('reset_password_token_created_at', time());
$this->save();
return $token;
}
public function getExtraData($key = null, $default = null)
{
return $key === null ? $this->extra_data : fnGet($this->extra_data, $key, $default);
}
public function getResetPasswordToken()
{
return $this->getExtraData('reset_password_token');
}
public function getResetPasswordTokenCreatedAt()
{
return $this->getExtraData('reset_password_token_created_at');
}
public function initialize()
{
$this->belongsTo('user_id', User::class, 'id', ['alias' => 'user']);
parent::initialize();
}
protected function prepareSave()
{
$this->getData('avatar') or $this->setData('avatar', $this->generateAvatarUrl());
$this->extra_data or $this->extra_data = [];
parent::prepareSave();
}
public function removeResetPasswordToken()
{
$this->setExtraData('reset_password_token');
$this->setExtraData('reset_password_token_created_at');
$this->save();
}
public function setExtraData($key, $value = null)
{
is_array($key) ? $this->extra_data = $key : array_set($this->extra_data, $key, $value);
return $this;
}
}
<?php
namespace Phwoolcon;
class Payload
{
use PayloadTrait;
}
<?php
namespace Phwoolcon;
use ErrorException;
trait PayloadTrait
{
protected $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function __call($method, $arguments)
{
if (($prefix = substr($method, 0, 3)) == 'get') {
$property = Text::uncamelize(substr($method, 3));
return $this->getData($property);
} elseif ($prefix == 'set') {
$property = Text::uncamelize(substr($method, 3));
return $this->setData($property, fnGet($arguments, 0));
}
throw new ErrorException('Call to undefined method ' . get_class($this) . '::' . $method . '()');
}
public static function create(array $data)
{
return new static($data);
}
public function getData($key = null, $default = null)
{
return $key === null ? $this->data : fnGet($this->data, $key, $default);
}
public function hasData($key)
{
return isset($this->data[$key]);
}
public function setData($key, $value = null)
{
if (is_array($key)) {
$this->data = $key;
} else {
$this->data[$key] = $value;
}
return $this;
}
}
<?php
namespace Phwoolcon\Protocol;
interface StreamWrapperInterface
{
public function __construct();
/**
* Close directory handle
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function dir_closedir();
/**
* Open directory handle
*
* @param string $path Specifies the URL that was passed to opendir().
* @param int $options Whether or not to enforce safe_mode (0x04).
*
* @return bool
*/
public function dir_opendir($path, $options);
/**
* Read entry from directory handle
*
* @return string|false Should return string representing the next filename, or FALSE if there is no next file.
*/
public function dir_readdir();
/**
* Rewind directory handle
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function dir_rewinddir();
/**
* Create a directory
*
* @param string $path Directory which should be created.
* @param int $mode The value passed to mkdir().
* @param int $options A bitwise mask of values, such as STREAM_MKDIR_RECURSIVE.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function mkdir($path, $mode, $options);
/**
* Renames a file or directory
*
* @param string $pathFrom The URL to the current file.
* @param string $pathTo he URL which the $pathFrom should be renamed to.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function rename($pathFrom, $pathTo);
/**
* Removes a directory
*
* @param string $path The directory URL which should be removed.
* @param int $options A bitwise mask of values, such as STREAM_MKDIR_RECURSIVE.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function rmdir($path, $options);
/**
* Retrieve the underlaying resource
*
* @param int $castAs STREAM_CAST_FOR_SELECT or STREAM_CAST_AS_STREAM
*
* @return resource|false Should return the underlying stream resource used by the wrapper, or FALSE.
*/
public function stream_cast($castAs);
/**
* Close a resource
*/
public function stream_close();
/**
* Tests for end-of-file on a file pointer
*
* @return bool Should return TRUE if the read/write position is at the end of the stream and if no more data is
* available to be read, or FALSE otherwise.
*/
public function stream_eof();
/**
* Flushes the output
*
* @return bool Should return TRUE if the cached data was successfully stored (or if there was no data to store),
* or FALSE if the data could not be stored.
*/
public function stream_flush();
/**
* Advisory file locking
*
* @param int $operation LOCK_SH to acquire a shared lock (reader).
* LOCK_EX to acquire an exclusive lock (writer).
* LOCK_UN to release a lock (shared or exclusive).
* LOCK_NB if you don't want flock() to block while locking. (not supported on Windows)
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function stream_lock($operation);
/**
* Change stream options
*
* @param string $path The file path or URL to set metadata.
* @param int $option One of: STREAM_META_TOUCH, STREAM_META_OWNER_NAME, STREAM_META_OWNER,
* STREAM_META_GROUP_NAME, STREAM_META_GROUP, STREAM_META_ACCESS
* @param mixed $value If option is:
* STREAM_META_TOUCH: Array consisting of two arguments of the touch() function.
* STREAM_META_OWNER_NAME, STREAM_META_GROUP_NAME: The name of the owner user/group as string.
* STREAM_META_OWNER, STREAM_META_GROUP: The value owner user/group argument as integer.
* STREAM_META_ACCESS: The argument of the chmod() as integer.
*
* @return bool Returns TRUE on success or FALSE on failure. If option is not implemented, FALSE should be
* returned.
*/
public function stream_metadata($path, $option, $value);
/**
* Opens file or URL
*
* @param string $path Specifies the URL that was passed to the original function.
* @param string $mode The mode used to open the file, as detailed for fopen().
* @param int $options STREAM_USE_PATH or STREAM_REPORT_ERRORS
* @param string &$openedPath If the path is opened successfully, and STREAM_USE_PATH is set in options,
* opened_path should be set to the full path of the file/resource that was actually
* opened.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function stream_open($path, $mode, $options, &$openedPath);
/**
* Read from stream
*
* @param int $count How many bytes of data from the current position should be returned.
* @return string|false If there are less than count bytes available, return as many as are available.
* If no more data is available, return either FALSE or an empty string.
*/
public function stream_read($count);
/**
* Seeks to specific location in a stream
*
* @param int $offset The stream offset to seek to.
* @param int $whence SEEK_SET, SEEK_CUR or SEEK_END
*
* @return bool Return TRUE if the position was updated, FALSE otherwise.
*/
public function stream_seek($offset, $whence = SEEK_SET);
/**
* Change stream options
*
* @param int $option STREAM_OPTION_BLOCKING, STREAM_OPTION_READ_TIMEOUT or STREAM_OPTION_WRITE_BUFFER
* @param int $arg1 If option is:
* STREAM_OPTION_BLOCKING: requested blocking mode (1 meaning block 0 not blocking).
* STREAM_OPTION_READ_TIMEOUT: the timeout in seconds.
* STREAM_OPTION_WRITE_BUFFER: buffer mode (STREAM_BUFFER_NONE or STREAM_BUFFER_FULL).
* @param int $arg2 If option is:
* STREAM_OPTION_BLOCKING: This option is not set.
* STREAM_OPTION_READ_TIMEOUT: the timeout in microseconds.
* STREAM_OPTION_WRITE_BUFFER: the requested buffer size.
*
* @return bool Returns TRUE on success or FALSE on failure. If option is not implemented, FALSE should be returned.
*/
public function stream_set_option($option, $arg1, $arg2);
/**
* Retrieve information about a file resource
*
* @return array See stat().
*
* @see stat()
*/
public function stream_stat();
/**
* Retrieve the current position of a stream
*
* @return int Should return the current position of the stream.
*/
public function stream_tell();
/**
* Truncate stream
*
* @param int $newSize The new size.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function stream_truncate($newSize);
/**
* Write to stream
*
* @param string $data The data should be stored into the underlying stream.
*
* @return int Should return the number of bytes that were successfully stored, or 0 if none could be stored.
*/
public function stream_write($data);
/**
* Delete a file
*
* @param string $path The file URL which should be deleted.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function unlink($path);
/**
* Retrieve information about a file
*
* @param string $path The file path or URL to stat.
* @param int $flags STREAM_URL_STAT_LINK or STREAM_URL_STAT_QUIET
*
* @return array Should return as many elements as stat() does.
* Unknown or unavailable values should be set to a rational value (usually 0).
*/
public function url_stat($path, $flags);
}
<?php
namespace Phwoolcon\Protocol;
trait StreamWrapperTrait
{
/**
* he current context, or NULL if no context was passed to the caller function.
*
* @var resource
*/
public $context;
protected $path;
protected $cursor = 0;
protected $eof = false;
protected $length = 0;
protected $stat = [
'dev' => 0, // device number
'ino' => 0, // inode number
'mode' => 0, // inode protection mode
'nlink' => 0, // number of links
'uid' => 0,
'gid' => 0,
'rdev' => 0, // device type, if inode device
'size' => 0, // size in bytes
'atime' => 0, // time of last access
'mtime' => 0, // time of last modification
'ctime' => 0, // time of last inode change
'blksize' => 0, // blocksize of filesystem IO
'blocks' => 0, // number of 512-byte blocks allocated
];
private function pleaseImplementRealMethod($method)
{
trigger_error('Please implement real method ' . $method, E_USER_WARNING);
}
public function __construct()
{
}
/**
* Close directory handle
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function dir_closedir()
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Open directory handle
*
* @param string $path Specifies the URL that was passed to opendir().
* @param int $options Whether or not to enforce safe_mode (0x04).
*
* @return bool
*/
public function dir_opendir($path, $options)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Read entry from directory handle
*
* @return string|false Should return string representing the next filename, or FALSE if there is no next file.
*/
public function dir_readdir()
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Rewind directory handle
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function dir_rewinddir()
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Create a directory
*
* @param string $path Directory which should be created.
* @param int $mode The value passed to mkdir().
* @param int $options A bitwise mask of values, such as STREAM_MKDIR_RECURSIVE.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function mkdir($path, $mode, $options)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Renames a file or directory
*
* @param string $pathFrom The URL to the current file.
* @param string $pathTo he URL which the $pathFrom should be renamed to.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function rename($pathFrom, $pathTo)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Removes a directory
*
* @param string $path The directory URL which should be removed.
* @param int $options A bitwise mask of values, such as STREAM_MKDIR_RECURSIVE.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function rmdir($path, $options)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Retrieve the underlaying resource
*
* @param int $castAs STREAM_CAST_FOR_SELECT or STREAM_CAST_AS_STREAM
*
* @return resource|false Should return the underlying stream resource used by the wrapper, or FALSE.
*/
public function stream_cast($castAs)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Close a resource
*/
public function stream_close()
{
}
/**
* Tests for end-of-file on a file pointer
*
* @return bool Should return TRUE if the read/write position is at the end of the stream and if no more data is
* available to be read, or FALSE otherwise.
*/
public function stream_eof()
{
return $this->eof;
}
/**
* Flushes the output
*
* @return bool Should return TRUE if the cached data was successfully stored (or if there was no data to store),
* or FALSE if the data could not be stored.
*/
public function stream_flush()
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Advisory file locking
*
* @param int $operation LOCK_SH to acquire a shared lock (reader).
* LOCK_EX to acquire an exclusive lock (writer).
* LOCK_UN to release a lock (shared or exclusive).
* LOCK_NB if you don't want flock() to block while locking. (not supported on Windows)
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function stream_lock($operation)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Change stream options
*
* @param string $path The file path or URL to set metadata.
* @param int $option One of: STREAM_META_TOUCH, STREAM_META_OWNER_NAME, STREAM_META_OWNER,
* STREAM_META_GROUP_NAME, STREAM_META_GROUP, STREAM_META_ACCESS
* @param mixed $value If option is:
* STREAM_META_TOUCH: Array consisting of two arguments of the touch() function.
* STREAM_META_OWNER_NAME, STREAM_META_GROUP_NAME: The name of the owner user/group as string.
* STREAM_META_OWNER, STREAM_META_GROUP: The value owner user/group argument as integer.
* STREAM_META_ACCESS: The argument of the chmod() as integer.
*
* @return bool Returns TRUE on success or FALSE on failure. If option is not implemented, FALSE should be
* returned.
*/
public function stream_metadata($path, $option, $value)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Opens file or URL
*
* @param string $path Specifies the URL that was passed to the original function.
* @param string $mode The mode used to open the file, as detailed for fopen().
* @param int $options STREAM_USE_PATH or STREAM_REPORT_ERRORS
* @param string &$openedPath If the path is opened successfully, and STREAM_USE_PATH is set in options,
* opened_path should be set to the full path of the file/resource that was actually
* opened.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function stream_open($path, $mode, $options, &$openedPath)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Read from stream
*
* @param int $count How many bytes of data from the current position should be returned.
* @return string|false If there are less than count bytes available, return as many as are available.
* If no more data is available, return either FALSE or an empty string.
*/
public function stream_read($count)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Seeks to specific location in a stream
*
* @param int $offset The stream offset to seek to.
* @param int $whence SEEK_SET, SEEK_CUR or SEEK_END
*
* @return bool Return TRUE if the position was updated, FALSE otherwise.
*/
public function stream_seek($offset, $whence = SEEK_SET)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Change stream options
*
* @param int $option STREAM_OPTION_BLOCKING, STREAM_OPTION_READ_TIMEOUT or STREAM_OPTION_WRITE_BUFFER
* @param int $arg1 If option is:
* STREAM_OPTION_BLOCKING: requested blocking mode (1 meaning block 0 not blocking).
* STREAM_OPTION_READ_TIMEOUT: the timeout in seconds.
* STREAM_OPTION_WRITE_BUFFER: buffer mode (STREAM_BUFFER_NONE or STREAM_BUFFER_FULL).
* @param int $arg2 If option is:
* STREAM_OPTION_BLOCKING: This option is not set.
* STREAM_OPTION_READ_TIMEOUT: the timeout in microseconds.
* STREAM_OPTION_WRITE_BUFFER: the requested buffer size.
*
* @return bool Returns TRUE on success or FALSE on failure. If option is not implemented, FALSE should be returned.
*/
public function stream_set_option($option, $arg1, $arg2)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Retrieve information about a file resource
*
* @return array See stat().
*
* @see stat()
*/
public function stream_stat()
{
return $this->stat;
}
/**
* Retrieve the current position of a stream
*
* @return int Should return the current position of the stream.
*/
public function stream_tell()
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Truncate stream
*
* @param int $newSize The new size.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function stream_truncate($newSize)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Write to stream
*
* @param string $data The data should be stored into the underlying stream.
*
* @return int Should return the number of bytes that were successfully stored, or 0 if none could be stored.
*/
public function stream_write($data)
{
$length = strlen($data);
$this->length += $length;
$this->stat['size'] = $this->length;
$this->stat['mtime'] = time();
return $length;
}
/**
* Delete a file
*
* @param string $path The file URL which should be deleted.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function unlink($path)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
/**
* Retrieve information about a file
*
* @param string $path The file path or URL to stat.
* @param int $flags STREAM_URL_STAT_LINK or STREAM_URL_STAT_QUIET
*
* @return array Should return as many elements as stat() does.
* Unknown or unavailable values should be set to a rational value (usually 0).
*/
public function url_stat($path, $flags)
{
$this->pleaseImplementRealMethod(__METHOD__);
}
protected function updateCursor($offset)
{
$this->eof = $this->cursor + $offset >= $this->length;
$this->cursor += $offset;
$this->eof and $this->cursor = $this->length;
}
}
<?php
namespace Phwoolcon;
use Phalcon\Di;
use Phwoolcon\Exception\InvalidConfigException;
use Phwoolcon\Queue\AdapterInterface;
use Phwoolcon\Queue\AdapterTrait;
use Phwoolcon\Queue\FailedLoggerDb;
use Workerman\MySQL\Connection as MySQL;
class Queue
{
/**
* @var Di
*/
protected static $di;
/**
* @var static
*/
protected static $instance;
protected $config;
/**
* @var AdapterTrait[]|AdapterInterface[]
*/
protected $connections = [];
public function __construct($config)
{
//为避免消息队列中mysql的连接长时间没有通讯而被mysql服务端踢掉,所以在消息队列中使用该数据库类,该类当发生mysql gone away错误时,会自动重试一次
global $mysql;
$mysqlConfig = Config::get('database.connections.mysql');
$mysql = new MySQL($mysqlConfig['host'], empty($mysqlConfig['port']) ? 3306 : $mysqlConfig['port'], $mysqlConfig['username'], $mysqlConfig['password'], $mysqlConfig['dbname'], $mysqlConfig['charset']); //实例化mysql组件并连接
$this->config = $config;
static::$di->has('queueFailLogger') or static::$di->setShared('queueFailLogger', function () use ($config) {
$class = $config['failed_logger']['adapter'];
return new $class($config['failed_logger']['options']);
});
}
/**
* @param string $name
* @return AdapterTrait|AdapterInterface
*/
protected function connect($name)
{
$queue = $this->config['queues'][$name];
$connectionName = $queue['connection'];
$connection = $this->config['connections'][$connectionName];
$options = array_merge($connection, $queue['options']);
$class = $connection['adapter'];
// @codeCoverageIgnoreStart
if (!$class || !class_exists($class)) {
throw new InvalidConfigException("Invalid queue adapter {$class}, please check config file queue.php");
}
// @codeCoverageIgnoreEnd
$instance = new $class(static::$di, $options, $name);
// @codeCoverageIgnoreStart
if (!$instance instanceof AdapterInterface) {
throw new InvalidConfigException("Queue adapter {$class} should implement " . AdapterInterface::class);
}
// @codeCoverageIgnoreEnd
return $instance;
}
public static function connection($name = null)
{
static::$instance === null and static::$instance = static::$di->getShared('queue');
$queue = static::$instance;
$name = $name ?: $queue->config['default'];
if (!isset($queue->connections[$name])) {
$queue->connections[$name] = $queue->connect($name);
}
return $queue->connections[$name];
}
/**
* @return FailedLoggerDb
*/
public static function getFailLogger()
{
return static::$di->getShared('queueFailLogger');
}
public static function register(Di $di)
{
static::$di = $di;
$di->remove('queue');
static::$instance = null;
$di->setShared('queue', function () {
return new static(Config::get('queue'));
});
}
}
<?php
namespace Phwoolcon\Queue\Adapter;
use Pheanstalk\Pheanstalk;
use Pheanstalk\Job as PheanstalkJob;
use Phwoolcon\Queue\Adapter\Beanstalkd\Job;
use Phwoolcon\Queue\AdapterInterface;
use Phwoolcon\Queue\AdapterTrait;
/**
* Class Beanstalkd
* @package Phwoolcon\Queue\Adapter
*
* @property Pheanstalk $connection
* @method Pheanstalk getConnection()
*/
class Beanstalkd implements AdapterInterface
{
use AdapterTrait;
protected $readTimeout = null;
protected function connect(array $options)
{
// @codeCoverageIgnoreStart
if ($this->connection) {
return;
}
// @codeCoverageIgnoreEnd
$this->connection = new Pheanstalk(
$options['host'],
$options['port'],
$options['connect_timeout'],
$options['persistence']
);
isset($options['read_timeout']) and $this->readTimeout = $options['read_timeout'];
}
public function pop($queue = null, $tags = null)
{
$queue = $this->getQueue($queue);
if (($job = $this->connection->watchOnly($queue)->reserve($this->readTimeout)) instanceof PheanstalkJob) {
return new Job($this, $job, $queue);
}
return null;
}
public function reserve($queue = null, $tags = null)
{
$queue = $this->getQueue($queue);
$listener = $this->connection->watchOnly($queue);
echo '----';
while (($job = $listener->reserve($this->readTimeout)) instanceof PheanstalkJob) {
return new Job($this, $job, $queue);
}
echo '-finish---';
return null;
}
public function pushRaw($payload, $queue = null, array $options = [])
{
$payload = (string)$payload;
$priority = isset($options['priority']) ? $options['priority'] : Pheanstalk::DEFAULT_PRIORITY;
$delay = isset($options['delay']) ? $options['delay'] : Pheanstalk::DEFAULT_DELAY;
$timeToRun = isset($options['ttr']) ? $options['ttr'] : Pheanstalk::DEFAULT_TTR;
return $this->connection->useTube($this->getQueue($queue))
->put($payload, $priority, $delay, $timeToRun);
}
public function delete($job)
{
$job = is_numeric($job) ? $job : $this->connection->peek($job);
return $this->connection->delete($job);
}
}
<?php
namespace Phwoolcon\Queue\Adapter\Beanstalkd;
use Pheanstalk\Pheanstalk;
use Pheanstalk\Job as PheanstalkJob;
use Phwoolcon\Queue\Adapter\Beanstalkd;
use Phwoolcon\Queue\Adapter\JobInterface;
use Phwoolcon\Queue\Adapter\JobTrait;
/**
* Class Job
* @package Phwoolcon\Queue\Adapter\Beanstalkd
*
* @property Pheanstalk $connection
* @property Beanstalkd $queue
* @property PheanstalkJob $rawJob
* @method PheanstalkJob getRawJob()
*/
class Job implements JobInterface
{
use JobTrait;
protected function _delete()
{
$this->connection->delete($this->rawJob);
}
protected function _release($delay = 0)
{
$priority = Pheanstalk::DEFAULT_PRIORITY;
$this->connection->release($this->rawJob, $priority, $delay);
}
public function attempts()
{
$stats = $this->connection->statsJob($this->rawJob);
return (int)$stats->reserves;
}
/**
* Bury the job in the queue.
*
* @return void
*/
public function bury()
{
$this->connection->bury($this->rawJob);
}
/**
* Get the job identifier.
*
* @return integer
*/
public function getJobId()
{
return $this->rawJob->getId();
}
public function getRawBody()
{
return $this->rawJob->getData();
}
}
<?php
namespace Phwoolcon\Queue\Adapter;
use Phwoolcon\Queue\Adapter\DbQueue\Connection;
use Phwoolcon\Queue\Adapter\DbQueue\Job;
use Phwoolcon\Queue\AdapterInterface;
use Phwoolcon\Queue\AdapterTrait;
/**
* Queue adapter: DbQueue
* @package Phwoolcon\Queue\Adapter
*
* @property Connection $connection
*/
class DbQueue implements AdapterInterface
{
use AdapterTrait;
protected function connect(array $options)
{
$this->connection = new Connection();
$this->connection->setOptions($options);
}
public function pop($queue = null,$tags=null)
{
$queue = $this->getQueue($queue);
if (($job = $this->connection->reserve($queue)) instanceof Connection) {
return new Job($this, $job, $queue);
}
return null;
}
public function pushRaw($payload, $queue = null, array $options = [])
{
$payload = (string)$payload;
$queue = $this->getQueue($queue);
return $this->connection->push($queue, $payload, $options);
}
public function reserve($queue = null,$tags=null)
{
// TODO: Implement reserve() method.
}
}
<?php
namespace Phwoolcon\Queue\Adapter\DbQueue;
use Phalcon\Db\Column;
use Phalcon\Db\Index;
use Phwoolcon\Db;
use Phwoolcon\Model;
/**
* DB Queue Connection
* @package Phwoolcon\Queue\Adapter\DbQueue
*
* @property $meta
* @property $payload
* @property $reserved_until
* @property $status
* @property $tries
*/
class Connection extends Model
{
const STATUS_READY = 'ready';
const STATUS_RESERVED = 'reserved';
const STATUS_BURIED = 'buried';
const STATUS_DELETED = 'deleted';
protected $_jsonFields = ['meta'];
protected $options;
public function bury()
{
$this->status = static::STATUS_BURIED;
$this->reserved_until = 0;
$this->save();
return $this;
}
/**
* @codeCoverageIgnore
*/
protected function createTable()
{
$this->getWriteConnection()->createTable($this->_table, null, [
'columns' => [
new Column('id', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => true,
'primary' => true,
]),
new Column('queue', [
'type' => Column::TYPE_VARCHAR,
'size' => 255,
'notNull' => true,
]),
new Column('payload', [
'type' => 'LONGTEXT',
'notNull' => false,
]),
new Column('status', [
'type' => Column::TYPE_VARCHAR,
'size' => 255,
'notNull' => true,
]),
new Column('tries', [
'type' => Column::TYPE_INTEGER,
'size' => 2,
'unsigned' => true,
'notNull' => true,
'default' => '0',
]),
new Column('reserved_until', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => true,
]),
new Column('meta', [
'type' => Column::TYPE_TEXT,
'notNull' => false,
]),
new Column('created_at', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => true,
]),
new Column('updated_at', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => true,
]),
],
'indexes' => [
new Index('queue', ['queue', 'reserved_until']),
new Index('status', ['status']),
],
'options' => [
'TABLE_COLLATION' => Db::getDefaultTableCharset(),
],
]);
}
public function kick($max)
{
/* @var static $item */
foreach ($items = static::findSimple([
'status' => static::STATUS_BURIED,
], [], 'id ASC', null, $max) as $item) {
$item->release();
}
return count($items);
}
protected function onConstruct()
{
}
/**
* @param string $queue
* @param string $payload
* @param array $options
* @return string
*/
public function push($queue, $payload, array $options = [])
{
$item = clone $this;
isset($options['time_to_run']) or $options['time_to_run'] = fnGet($this->options, 'time_to_run', 60);
$item->addData([
'queue' => $queue,
'payload' => $payload,
'status' => 'ready',
'tries' => 0,
'reserved_until' => 0,
'meta' => $options,
])->save();
return $item->getId();
}
public function release()
{
$this->status = static::STATUS_READY;
$this->reserved_until = 0;
$this->save();
return $this;
}
/**
* @codeCoverageIgnore
* @param string $queue
*/
protected function releaseTimeoutJobs($queue)
{
/* @var static $item */
foreach (static::findSimple([
'queue' => $queue,
'reserved_until' => ['>=', time()],
'status' => static::STATUS_RESERVED,
], [], 'id ASC') as $item) {
$item->release();
}
}
/**
* @param string $queue
* @return $this|null
*/
public function reserve($queue)
{
$this->releaseTimeoutJobs($queue);
if ($item = static::findFirstSimple([
'queue' => $queue,
'status' => static::STATUS_READY,
], [], 'id ASC')) {
$item->status = static::STATUS_RESERVED;
++$item->tries;
$this->reserved_until = time() + fnGet($this->meta, 'time_to_run');
$item->save();
return $item;
}
return null;
}
public function setOptions(array $options)
{
$this->options = $options;
$table = fnGet($options, 'table', 'queue_jobs');
$this->_table = $table;
$this->setSource($table);
$this->getWriteConnection()->tableExists($table) or $this->createTable();
parent::onConstruct();
}
public function softDelete()
{
$this->status = static::STATUS_DELETED;
$this->reserved_until = 0;
$this->save();
return $this;
}
}
<?php
namespace Phwoolcon\Queue\Adapter\DbQueue;
use Phwoolcon\Queue\Adapter\JobInterface;
use Phwoolcon\Queue\Adapter\JobTrait;
/**
* DB Queue Job
* @package Phwoolcon\Queue\Adapter\DbQueue
*
* @property Connection $rawJob
*/
class Job implements JobInterface
{
use JobTrait;
protected function _delete()
{
$this->rawJob->softDelete();
}
protected function _release($delay = 0)
{
$this->rawJob->release();
}
public function attempts()
{
return (int)$this->rawJob->tries;
}
public function bury()
{
$this->rawJob->bury();
}
public function getJobId()
{
return $this->rawJob->getId();
}
public function getRawBody()
{
return $this->rawJob->payload;
}
}
<?php
namespace Phwoolcon\Queue\Adapter;
interface JobInterface
{
/**
* Fire the job.
*
* @param null $queue
* @return void
*/
public function fire($queue=null);
/**
* Delete the job from the queue.
*
* @return void
*/
public function delete();
/**
* Get the raw body of the job
*
* @return string
*/
public function getRawBody();
/**
* Release the job back into the queue.
*
* @param int $delay
* @return void
*/
public function release($delay = 0);
/**
* Get the number of times the job has been attempted.
*
* @return int
*/
public function attempts();
/**
* Get the name of the queued job class.
*
* @return string
*/
public function getName();
/**
* Get the name of the queue the job belongs to.
*
* @return string
*/
public function getQueueName();
}
<?php
namespace Phwoolcon\Queue\Adapter;
use Phwoolcon\Config;
use Phwoolcon\Queue\AdapterInterface;
use Phwoolcon\Queue\AdapterTrait;
trait JobTrait
{
/**
* The queue instance.
*
* @var AdapterTrait
*/
protected $queue;
/**
* The queue connection
*/
protected $connection;
/**
* The name of the queue the job belongs to.
*
* @var string
*/
protected $queueName;
/**
* The raw job instance.
*/
protected $rawJob;
/**
* Indicates if the job has been deleted.
*
* @var bool
*/
protected $deleted = false;
/**
* Indicates if the job has been released.
*
* @var bool
*/
protected $released = false;
/**
* JobTrait constructor.
* @param AdapterInterface|AdapterTrait $queue
* @param $rawJob
* @param string $queueName
*/
public function __construct($queue, $rawJob, $queueName)
{
$this->queue = $queue;
$this->connection = $queue->getConnection();
$this->rawJob = $rawJob;
$this->queueName = $queueName;
}
abstract protected function _delete();
abstract protected function _release($delay = 0);
public function delete()
{
$this->_delete();
$this->deleted = true;
}
public function fire($queue=null)
{
// @codeCoverageIgnoreStart
if ($worker = $this->queue->getPredefinedWorker()) {
$data = $this->getRawBody();
} //@codeCoverageIgnoreEnd
else {
$payload = json_decode($this->getRawBody(), true);
$worker = $payload['job'];
$data = $payload['data'];
}
if(!empty($worker)){
$workers[] = $worker;
}else {
$workers = Config::get('queue.workers.' . $queue);
}
if(!empty($workers)) {
foreach ($workers as $worker) {
if (is_string($worker)) {
if (strpos($worker, $separator = '::')) {
$worker = explode($separator, $worker, 2);
} elseif (strpos($worker, $separator = '@')) {
$worker = explode($separator, $worker, 2);
$worker[0] = $this->queue->getDi()->getShared($worker[0]);
} elseif (strpos($worker, $separator = '->')) {
$worker = explode($separator, $worker, 2);
$worker[0] = new $worker[0];
}
}
call_user_func($worker, $this, $data);
}
}
$this->delete();
}
public function getName()
{
return json_decode($this->getRawBody(), true)['job'];
}
/**
* @return AdapterInterface|AdapterTrait
* @codeCoverageIgnore
*/
public function getQueue()
{
return $this->queue;
}
/**
* @return string
* @codeCoverageIgnore
*/
public function getQueueName()
{
return $this->queueName;
}
/**
* Get the raw body string for the job.
*
* @return string
*/
abstract public function getRawBody();
/**
* @codeCoverageIgnore
*/
public function getRawJob()
{
return $this->rawJob;
}
/**
* Determine if the job has been deleted.
*
* @return bool
*/
public function isDeleted()
{
return $this->deleted;
}
public function release($delay = 0)
{
$this->_release($delay);
$this->released = true;
}
}
<?php
/**
* Created by IntelliJ IDEA.
* User: yeran
* Date: 2018/5/11
* Time: 下午6:34
*/
namespace Phwoolcon\Queue\Adapter;
use Phwoolcon\Queue\Adapter\RocketMQ\Job;
use Phwoolcon\Queue\Adapter\RocketMQ\RocketClient;
use Phwoolcon\Queue\AdapterInterface;
use Phwoolcon\Queue\AdapterTrait;
class RocketMQ implements AdapterInterface
{
use AdapterTrait;
protected $readTimeout = null;
/**
* @var RocketClient
*/
private $connection;
protected function connect(array $options)
{
// @codeCoverageIgnoreStart
if ($this->connection) {
return;
}
// @codeCoverageIgnoreEnd
$this->connection = new RocketClient(
$options['host'],
$options['port'],
$options['connect_timeout'],
$options['persistence']
);
isset($options['read_timeout']) and $this->readTimeout = $options['read_timeout'];
}
public function pop($queue = null,$tags=null)
{
$queue = $this->getQueue($queue);
if (($job = $this->connection->watchOnly($queue,$tags)->reserve($this->readTimeout))) {
return new Job($this, $job, $queue);
}
return null;
}
public function reserve($queue = null,$tags=null){
$queue = $this->getQueue($queue);
$listener = $this->connection->watchOnly($queue,$tags);
echo '----';
while (($job = $listener->reserve($this->readTimeout))) {
return new Job($this, $job, $queue);
}
echo '-finish---';
return null;
}
public function pushRaw($payload, $queue = null, array $options = [])
{
$payload = (string)$payload;
$priority = isset($options['priority']) ? $options['priority'] : Pheanstalk::DEFAULT_PRIORITY;
$delay = isset($options['delay']) ? $options['delay'] : Pheanstalk::DEFAULT_DELAY;
$timeToRun = isset($options['ttr']) ? $options['ttr'] : Pheanstalk::DEFAULT_TTR;
return $this->connection->useTube($this->getQueue($queue))
->put($payload, $priority, $delay, $timeToRun);
}
}
\ No newline at end of file
<?php
namespace Phwoolcon\Queue\Adapter\RocketMQ;
use Phwoolcon\Queue\Adapter\JobInterface;
use Phwoolcon\Queue\Adapter\JobTrait;
class Job implements JobInterface
{
use JobTrait;
protected function _delete()
{
$this->connection->delete($this->rawJob);
}
protected function _release($delay = 0)
{
$priority = Pheanstalk::DEFAULT_PRIORITY;
$this->connection->release($this->rawJob, $priority, $delay);
}
public function attempts()
{
$stats = $this->connection->statsJob($this->rawJob);
return (int)$stats->reserves;
}
/**
* Bury the job in the queue.
*
* @return void
*/
public function bury()
{
$this->connection->bury($this->rawJob);
}
/**
* Get the job identifier.
*
* @return integer
*/
public function getJobId()
{
return $this->rawJob->getId();
}
public function getRawBody()
{
return $this->rawJob->getData();
}
}
<?php
/**
* Created by IntelliJ IDEA.
* User: yeran
* Date: 2018/5/12
* Time: 上午12:52
*/
namespace Phwoolcon\Queue\Adapter\RocketMQ;
use core\service\asyn\tcp\ClientPool;
class RocketClient
{
/**
* @var
*/
private $tcpClient;
public function __construct( $host, $port, $connect_timeout,$persistence)
{
// 注册tcp连接,默认执行连接
$this->tcpClient = ClientPool::getInstance()->getAsyncClient();
}
/**
* 注册监听topic和tags
*
* @param $topic
* @param $tags
* @return $this
*/
public function watchOnly($topic,$tags,$callback=null){
// 发送第一次tcp请求,声明监听topic和tags
$data = [
'msg' => [
'topic' => $topic,
'tags' => $tags
],
'access' =>[
'appid' => 'LD_yeran001',
'accessKey' => ''
]
];
ClientPool::getInstance()->send($this->tcpClient,json_encode($data), function ($result) use ($callback) {
if($callback){
call_user_func($callback,$result);
}
return $this;
});
}
/**
* 启动阻塞监听,如果收到新的消息,会通过回调传入
*
* @param $readTimeout
*/
public function reserve($readTimeout){
// 启动消息结果回调
return new RowJob(1,"dasda");
}
/**
*
* 发送消息至队列
*
* @param $msg
*/
public function send($msg){
}
}
\ No newline at end of file
<?php
/**
* Created by IntelliJ IDEA.
* User: yeran
* Date: 2018/5/12
* Time: 上午1:15
*/
namespace Phwoolcon\Queue\Adapter\RocketMQ;
class RowJob
{
private $messageId;
private $data;
/**
* RowJob constructor.
* @param $messageId
* @param $data
*/
public function __construct($messageId, $data)
{
$this->messageId = $messageId;
$this->data = $data;
}
/**
* @return mixed
*/
public function getMessageId()
{
return $this->messageId;
}
/**
* @param mixed $messageId
*/
public function setMessageId($messageId)
{
$this->messageId = $messageId;
}
/**
* @return mixed
*/
public function getData()
{
return $this->data;
}
/**
* @param mixed $data
*/
public function setData($data)
{
$this->data = $data;
}
}
\ No newline at end of file
<?php
namespace Phwoolcon\Queue;
use Phwoolcon\Queue\Adapter\JobInterface;
use Phwoolcon\Queue\Adapter\JobTrait;
interface AdapterInterface
{
/**
* Pop the next job off of the queue.
*
* @param string $queue
* @return JobInterface|JobTrait|null
*/
public function pop($queue = null, $tags = null);
public function reserve($queue = null, $tags = null);
/**
* Push a new job onto the queue.
*
* @param string $worker
* @param mixed $data
* @param string $queue
* @param array $options
* @return mixed
*/
public function push($worker, $data = '', $queue = null, array $options = []);
/**
* Push a raw payload onto the queue.
*
* @param string $payload
* @param string $queue
* @param array $options
* @return mixed
*/
public function pushRaw($payload, $queue = null, array $options = []);
/**
* delete job
* @param $job
* @return mixed
*/
public function delete($job);
}
<?php
namespace Phwoolcon\Queue;
use Phalcon\Di;
trait AdapterTrait
{
/**
* @var Di
*/
protected $di;
protected $connection;
protected $defaultQueue;
protected $options;
protected $connectionName;
protected $predefinedWorker;
public function __construct(Di $di, array $options, $connectionName)
{
$this->di = $di;
$this->options = $options;
$this->defaultQueue = $options['default'];
$this->connectionName = $connectionName;
isset($options['worker']) and $this->predefinedWorker = $options['worker'];
$this->connect($options);
}
abstract protected function connect(array $options);
/**
* Create a payload string from the given worker and data.
*
* @param string $worker
* @param mixed $data
* @return string
*/
protected function createPayload($worker, $data = '')
{
return json_encode(['job' => $worker, 'data' => $data]);
}
public function getConnection()
{
return $this->connection;
}
/**
* @return string
* @codeCoverageIgnore
*/
public function getConnectionName()
{
return $this->connectionName;
}
public function getPredefinedWorker()
{
return $this->predefinedWorker;
}
public function getDi()
{
return $this->di;
}
/**
* Get the queue or return the default.
*
* @param string|null $queue
* @return string
*/
public function getQueue($queue)
{
return $queue ?: $this->defaultQueue;
}
abstract public function pop($queue = null, $tags = null);
/**
* @param callable|string $worker
* @param mixed $data
* @param string $queue
* @param array $options
* @return mixed
*/
public function push($worker, $data = '', $queue = null, array $options = [])
{
return $this->pushRaw($this->createPayload($worker, $data), $queue, $options);
}
/**
* @param string $payload
* @param string $queue
* @param array $options
*/
abstract public function pushRaw($payload, $queue = null, array $options = []);
/**
* @param mixed $job
*/
abstract public function delete($job);
}
<?php
namespace Phwoolcon\Queue;
use Phalcon\Db\Column;
use Phwoolcon\Config;
use Phwoolcon\DateTime;
use Phwoolcon\Db;
class FailedLoggerDb
{
protected $table;
protected $options = [
'connection' => '',
'table' => 'failed_jobs',
];
public function __construct($options)
{
$this->options = $options;
$this->table = $options['table'];
$db = $this->getDb();
$db->tableExists($this->table) or $this->createTable();
Config::runningUnitTest() and $db->delete($this->table);
}
/**
* @codeCoverageIgnore
*/
protected function createTable()
{
$this->getDb()->createTable($this->table, null, [
'columns' => [
new Column('id', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => true,
'primary' => true,
'autoIncrement' => true,
]),
new Column('connection', [
'type' => Column::TYPE_VARCHAR,
'size' => 255,
'notNull' => true,
]),
new Column('queue', [
'type' => Column::TYPE_VARCHAR,
'size' => 255,
'notNull' => true,
]),
new Column('payload', [
'type' => 'LONGTEXT',
'notNull' => false,
]),
new Column('failed_at', [
'type' => Column::TYPE_TIMESTAMP,
'notNull' => true,
'default' => 'CURRENT_TIMESTAMP',
]),
],
'options' => [
'TABLE_COLLATION' => Db::getDefaultTableCharset(),
],
]);
}
protected function getDb()
{
return Db::connection($this->options['connection']);
}
/**
* Log a failed job into storage.
*
* @param string $connection
* @param string $queue
* @param string $payload
* @return void
*/
public function log($connection, $queue, $payload)
{
$failed_at = date(DateTime::MYSQL_DATETIME);
$this->getDb()->insertAsDict($this->table, compact('connection', 'queue', 'payload', 'failed_at'));
}
}
<?php
namespace Phwoolcon\Queue;
use Exception;
use Phwoolcon\Queue;
use Phwoolcon\Queue\Adapter\JobInterface;
use Phwoolcon\Queue\Adapter\JobTrait;
use Phwoolcon\Queue\Listener\Result;
use Throwable;
class Listener
{
/**
* Log a failed job into storage.
*
* @param JobInterface|JobTrait $job
*/
protected function fail($job)
{
Queue::getFailLogger()->log($job->getQueue()->getConnectionName(), $job->getQueueName(), $job->getRawBody());
$job->delete();
}
/**
* Listen to the given queue.
*
* @param string $connectionName
* @param string $queue
* @param int $delay
* @param int $sleep
* @param int $maxTries
* @param null $tags
* @return Result
*/
public function pop($connectionName, $queue = null, $delay = 0, $sleep = 3, $maxTries = 0, $tags = null)
{
$connection = Queue::connection($connectionName);
if ($job = $connection->pop($queue, $tags)) {
return $this->process($queue, $job, $maxTries, $delay);
}
// Sleep the worker for the specified number of seconds, if no jobs
sleep($sleep);
return Result::success(null);
}
/**
*
* 阻塞监听
*
* @param $connectionName
* @param null $queue
* @param int $delay
* @param int $sleep
* @param int $maxTries
* @param null $tags
* @return bool
*/
public function reserve($connectionName, $queue = null, $delay = 0, $sleep = 3, $maxTries = 0, $tags = null)
{
$connection = Queue::connection($connectionName);
while ($job = $connection->reserve($queue, $tags)) {
$this->process($queue, $job, $maxTries, $delay);
return true;
}
return false;
}
/**
* Process a given job from the queue.
*
* @param $queue
* @param JobInterface|JobTrait $job
* @param int $maxTries
* @param int $delay
* @return Result
* @throws Exception
* @throws Throwable
*/
public function process($queue, JobInterface $job, $maxTries = 0, $delay = 0)
{
if ($maxTries > 0 && $job->attempts() > $maxTries) {
$this->fail($job);
return Result::failed($job);
}
try {
$job->fire($queue);
return Result::success($job);
} catch (Exception $e) {
// If we catch an exception, we will attempt to release the job back onto
// the queue so it is not lost. This will let is be retried at a later
// time by another listener (or the same one). We will do that here.
if (!$job->isDeleted()) {
$job->release($delay);
}
commandLineOutput($e->getFile() . ':' . $e->getLine() . ' ' . $e->getMessage(), 'ERROR');
// throw $e;
} // @codeCoverageIgnoreStart
catch (Throwable $e) {
if (!$job->isDeleted()) {
$job->release($delay);
}
commandLineOutput($e->getFile() . ':' . $e->getLine() . ' ' . $e->getMessage(), 'ERROR');
// throw $e;
}
// @codeCoverageIgnoreEnd
}
}
<?php
namespace Phwoolcon\Queue\Listener;
use Phwoolcon\Queue\Adapter\JobInterface;
class Result
{
const STATUS_SUCCESS = 1;
const STATUS_FAILED = 0;
/**
* @var JobInterface|null
*/
protected $job;
protected $status = false;
public function __construct(JobInterface $job = null, $status = self::STATUS_SUCCESS)
{
$this->job = $job;
$this->status = $status;
}
public static function failed(JobInterface $job = null)
{
return new static($job, static::STATUS_FAILED);
}
public function getJob()
{
return $this->job;
}
public function getStatus()
{
return $this->status;
}
public static function success(JobInterface $job = null)
{
return new static($job, static::STATUS_SUCCESS);
}
}
<?php
namespace Phwoolcon;
use Closure;
use core\base\Exception\ApiException;
use Opis\Closure\SerializableClosure;
use Phalcon\Di;
use Phalcon\Http\Request;
use Phalcon\Http\Response;
use Phalcon\Mvc\Router as PhalconRouter;
use Phalcon\Mvc\Router\Route;
use Phwoolcon\Daemon\ServiceAwareInterface;
use Phwoolcon\Exception\Http\CsrfException;
use Phwoolcon\Exception\Http\NotFoundException;
use Phwoolcon\Exception\HttpException;
use Phwoolcon\Fsm\Exception;
/**
* Class Router
*
* @package Phwoolcon
*
* @property Route[] $_routes
* @method Route add($pattern, $paths = null, $httpMethods = null, $position = Router::POSITION_LAST)
*/
class Router extends PhalconRouter implements ServiceAwareInterface
{
/**
* @var Di
*/
protected static $di;
protected static $disableSession = true;
protected static $disableCsrfCheck = false;
protected static $runningUnitTest = false;
protected static $useLiteHandler = true;
protected static $currentUri = null;
/**
* @var Request
*/
protected static $request;
/**
* @var static
*/
protected static $router;
protected $_uriSource = self::URI_SOURCE_SERVER_REQUEST_URI;
protected $_sitePathPrefix;
protected $_sitePathLength;
protected static $cacheFile;
/**
* @var Response\Cookies
*/
protected $cookies;
/**
* @var Response
*/
protected $response;
/**
* @var Route[][]
*/
protected $exactRoutes;
/**
* @var Route[][]
*/
protected $regexRoutes;
public function __construct()
{
parent::__construct(false);
static::$runningUnitTest = Config::runningUnitTest();
static::$useLiteHandler = Config::get('app.use_lite_router');
// @codeCoverageIgnoreStart
if ($this->_sitePathPrefix = Config::get('app.site_path')) {
$this->_uriSource = self::URI_SOURCE_GET_URL;
$this->_sitePathLength = strlen($this->_sitePathPrefix);
}
// @codeCoverageIgnoreEnd
$this->removeExtraSlashes(true);
if (Config::get('app.cache_routes')) {
if (!$this->loadLocalCache()) {
$this->loadRoutes();
$this->saveLocalCache();
}
} else {
$this->loadRoutes();
}
$this->cookies = static::$di->getShared('cookies');
$this->response = static::$di->getShared('response');
$this->response->setStatusCode(200);
}
/* public function addRoutes(array $routes, $prefix = null, $filter = null)
{
$prefix and $prefix = rtrim($prefix, '/');
foreach ($routes as $method => $methodRoutes) {
foreach ($methodRoutes as $uri => $handler) {
$uri{0} == '/' or $uri = '/' . $uri;
$prefix and $uri = $prefix . $uri;
$uri == '/' or $uri = rtrim($uri, '/');
$this->quickAdd($method, $uri, $handler, $filter);
}
}
}*/
public function addRoutes(array $routes)
{
foreach ($routes as $route) {
if (!$route[2] instanceof Closure) {
$pattern = $route[1];
$paths = $route[2];
$method = $route[0];
$filter = empty($route[3]) ? null : $route[3];
$this->quickAdd($pattern, $paths, $method, $filter);
} else {
$pattern = $route[1];
$function = $route[2];
$method = $route[0];
$filter = empty($route[3]) ? null : $route[3];
$this->CquickAdd($pattern, $function, $method, $filter);
}
}
}
public static function checkCsrfToken()
{
static::$request or static::$request = static::$di->getShared('request');
$request = static::$request;
if ($request->isPost() && $request->get('_token') != Session::getCsrfToken()) {
self::throwCsrfException();
}
}
public static function clearCache()
{
is_file($file = static::$cacheFile) and unlink($file);
}
public static function disableCsrfCheck()
{
static::$disableCsrfCheck = true;
}
public static function disableSession()
{
static::$disableSession = true;
}
/* public static function dispatch($uri = null)
{
try {
static::$router === null and static::$router = static::$di->getShared('router');
$router = static::$router;
// @codeCoverageIgnoreStart
if (!$uri && $router->_sitePathLength && $router->_uriSource == self::URI_SOURCE_GET_URL) {
list($uri) = explode('?', $_SERVER['REQUEST_URI']);
$uri = str_replace(basename($_SERVER['SCRIPT_FILENAME']), '', $uri);
if (substr($uri, 0, $router->_sitePathLength) == $router->_sitePathPrefix) {
$uri = substr($uri, $router->_sitePathLength);
}
}
// @codeCoverageIgnoreEnd
Events::fire('router:before_dispatch', $router, ['uri' => $uri]);
$realUri = $uri === null ? $router->getRewriteUri() : $uri;
$handledUri = $realUri === '/' ? $realUri : rtrim($realUri, '/');
static::$currentUri = $handledUri;
static::$useLiteHandler ? $router->liteHandle($handledUri) : $router->handle($handledUri);
($route = $router->getMatchedRoute()) or static::throw404Exception();
$controllerClass = $router->getControllerName();
$controllerClass instanceof SerializableClosure and $controllerClass = $controllerClass->getClosure();
if ($controllerClass instanceof Closure) {
static::$disableSession or Session::start();
static::$disableCsrfCheck or static::checkCsrfToken();
$response = $controllerClass();
if (!$response instanceof Response) {
// @var Response $realResponse
$realResponse = $router->response;
$realResponse->setContent($response);
$response = $realResponse;
}
} else {
// @var Controller $controller
$controller = new $controllerClass;
method_exists($controller, 'initialize') and $controller->initialize();
static::$disableSession or Session::start();
static::$disableCsrfCheck or static::checkCsrfToken();
method_exists($controller, $method = $router->getActionName()) or static::throw404Exception();
$controller->{$method}();
$response = $controller->response;
}
Events::fire('router:after_dispatch', $router, ['response' => $response]);
Session::end();
return $response;
} catch (HttpException $e) {
Log::exception($e);
return static::$runningUnitTest ? $e : $e->toResponse();
}
}*/
public static function dispatch($uri = null)
{
try {
static::$router === null and static::$router = static::$di->getShared('router');
$router = static::$router;
// @codeCoverageIgnoreStart
if (!$uri && $router->_sitePathLength && $router->_uriSource == self::URI_SOURCE_GET_URL) {
list($uri) = explode('?', $_SERVER['REQUEST_URI']);
$uri = str_replace(basename($_SERVER['SCRIPT_FILENAME']), '', $uri);
if (substr($uri, 0, $router->_sitePathLength) == $router->_sitePathPrefix) {
$uri = substr($uri, $router->_sitePathLength);
}
}
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
$httpException = new HttpException(null, 204, ['Access-Control-Allow-Method:GET, POST', 'Access-Control-Request-Headers:accept, content-type, token, key']);
return $httpException->toResponse();
}
// @codeCoverageIgnoreEnd
Events::fire('router:before_dispatch', $router, ['uri' => $uri]);
$realUri = $uri === null ? $router->getRewriteUri() : $uri;
$handledUri = $realUri === '/' ? $realUri : rtrim($realUri, '/');
static::$currentUri = $handledUri;
static::$useLiteHandler ? $router->liteHandle($handledUri) : $router->handle($handledUri);
($route = $router->getMatchedRoute()) or static::throw404Exception();
//此处匹配了route正则规则,不执行static::throw404Exception();
//采用的是原来传入函数形式的话则执行后面的404(在我的电脑上显示404,原因是我的访问地址经过了ld,重写apache规则后无404);
$module = $router->getModulename();
if (!empty($module)) {
$moduleDefinition = MODULES_PATH . '/' . $module . '/Module.php';
if (is_file($moduleDefinition)) {
$moduleClass = 'Lestore\\' . ucfirst($module) . '\\Module';
$moduleObject = new $moduleClass;
$moduleObject->registerAutoloaders(static::$di);
$moduleObject->registerServices(static::$di);
}
}
// $namespace = $router->getNamespaceName();
$controllerName = $router->getControllerName();
// die($controllerName);
$controllerName instanceof SerializableClosure ? $controllerClass = $controllerName->getClosure() : $controllerClass = $controllerName;
if ($controllerClass instanceof Closure) {
static::$disableSession or Session::start();
static::$disableCsrfCheck or static::checkCsrfToken();
$response = $controllerClass();
if (!$response instanceof Response) {
// @var Response $realResponse
$realResponse = $router->response;
$realResponse->setContent($response);
$response = $realResponse;
}
} else {
if (empty($controllerName)) {
$controllerName = 'index';
}
if (!strpos($controllerName, "Controller")) {
$controllerName = ucfirst($controllerName) . "Controller";
}
$actionname = $router->getActionName() ? $router->getActionName() : "index";
$params = $router->getParams();
if (!empty($params)) {
foreach ($params as $key => $item) {
if (is_string($key)) {
unset($params[$key]);
}
}
}
if (array_key_exists('ptoken', $params)) unset($params['ptoken']);
$prefix = 'Lestore\\';
if ($module) {
$prefix .= ucfirst($module);
}
$controllerClass = $prefix . "\Controllers\\" . $controllerName;
// $controllerClass = $prefix.ucfirst($namespace) . "\Controllers\\" . $controllerName;
/* @var Controller $controller */
// var_dump($controllerClass);exit;
// var_dump(class_exists($controllerClass));
// exit;
if (!class_exists($controllerClass)) {
$controllerClass = $prefix . "\\" . $controllerName;
// $controllerClass = $prefix.ucfirst($namespace) . "\\" . $controllerName;
}
// die($controllerClass);
$controller = new $controllerClass;
method_exists($controller, 'initialize') and $controller->initialize();
// static::$disableSession or Session::start();
static::$disableCsrfCheck or static::checkCsrfToken();
// var_dump($params);
// exit;
if (method_exists($controller, $actionname)) {
//...用于php5.6以上,变量为数组,且不能为空
empty($params) ? $controller->{$actionname}() : $controller->{$actionname}(...$params);
} elseif (method_exists($controller, $actionname . "Action")) {
empty($params) ? $controller->{$actionname}() : $controller->{$actionname}(...$params);
} else {
static::throw404Exception();
}
$response = $controller->response;
}
Events::fire('router:after_dispatch', $router, ['response' => $response]);
// Session::end();
return $response;
} catch (HttpException $e) {
Log::exception($e);
return $e->toResponse();
//return static::$runningUnitTest ? $e : $e->toResponse();
} catch (\Exception $exception) {
debug('[Exception] File:[' . $exception->getFile() . '] ' . 'Line:[' . $exception->getLine() . '] ' . 'Message:[' . $exception->getMessage() . '] ', 'ERROR');
//捕获异常
$response = new Response(); //获取响应实例
//判断是否为API异常
if ($exception instanceof ApiException) {
$response->setStatusCode(200); //设置HTTP状态码
//批量设置ApiException中获取的header
foreach ($exception->getHeaders() as $name => $value) {
if (is_numeric($name)) {
list($name, $value) = explode(':', $value);
}
$response->setHeader(trim($name), trim($value));
}
$response->setContent($exception->getBody()); //设置响应内容
} else {
return self::exceptionResponse($response, $exception->getMessage());
}
return $response;
}
}
/**
* @param string $template
* @param string $pateTitle
* @return string
*/
public static function generateErrorPage($template, $pateTitle)
{
return View::make('errors', $template, ['page_title' => $pateTitle]);
}
/**
* @codeCoverageIgnore
* @return string
*/
public static function getCurrentUri()
{
return self::$currentUri;
}
public function liteHandle($uri)
{
static::$request or static::$request = static::$di->getShared('request');
$request = static::$request;
$this->exactRoutes === null and $this->splitRoutes();
$this->_matches = null;
$this->_wasMatched = true;
$this->_matchedRoute = null;
$this->_namespace = $this->_defaultNamespace;
$this->_module = $this->_defaultModule;
$this->_controller = $this->_defaultController;
$this->_action = $this->_defaultAction;
$this->_params = $this->_defaultParams;
$httpMethod = $request->getMethod();
$matchedRoute = null;
if (isset($this->exactRoutes[$httpMethod][$uri])) {
$matchedRoute = $this->exactRoutes[$httpMethod][$uri];
if ($beforeMatch = $matchedRoute->getBeforeMatch()) {
if (!call_user_func_array($beforeMatch, [$uri, $matchedRoute, $this])) {
$matchedRoute = null;
}
}
}
if ($matchedRoute === null) {
$regexRoutes = isset($this->regexRoutes[$httpMethod]) ? $this->regexRoutes[$httpMethod] : [];
foreach ($regexRoutes as $pattern => $route) {
if (preg_match($pattern, $uri, $matches)) {
if ($beforeMatch = $route->getBeforeMatch()) {
// @codeCoverageIgnoreStart
if (!call_user_func_array($beforeMatch, [$uri, $route, $this])) {
continue;
}
// @codeCoverageIgnoreEnd
}
$paths = $route->getPaths();
$parts = $paths;
foreach ($paths as $part => $position) {
$isPositionInt = is_int($position);
$isPositionString = is_string($position);
if (!$isPositionInt && !$isPositionString) {
continue;
}
if (isset($matches[$position])) {
/**
* Update the parts
*/
$parts[$part] = $matches[$position];
} else {
/**
* Remove the path if the parameter was not matched
*/
// @codeCoverageIgnoreStart
if ($isPositionInt) {
unset($parts[$part]);
}
// @codeCoverageIgnoreEnd
}
}
/**
* Update the matches generated by preg_match
*/
$this->_matches = $matches;
$matchedRoute = $route;
break;
}
}
}
if ($matchedRoute) {
$this->_matchedRoute = $matchedRoute;
$this->_wasMatched = true;
isset($paths) or $paths = $matchedRoute->getPaths();
isset($parts) or $parts = $paths;
/**
* Check for a namespace
*/
if (isset($parts['namespace'])) {
$namespace = $parts['namespace'];
if (!is_numeric($namespace)) {
$this->_namespace = $namespace;
}
unset($parts['namespace']);
}
/**
* Check for a module
*/
if (isset($parts['module'])) {
$module = $parts['module'];
if (!is_numeric($module)) {
$this->_module = $module;
}
unset($parts['module']);
}
/**
* Check for a controller
*/
if (isset($parts['controller'])) {
$controller = $parts['controller'];
if (!is_numeric($controller)) {
$this->_controller = $controller;
}
unset($parts['controller']);
}
/**
* Check for an action
*/
if (isset($parts['action'])) {
$action = $parts['action'];
if (!is_numeric($action)) {
$this->_action = $action;
}
unset($parts['action']);
}
$params = [];
/**
* Check for parameters
*/
if (isset($parts['params'])) {
$paramsStr = $parts['params'];
if (is_string($paramsStr)) {
$strParams = trim($paramsStr, '/');
if ($strParams !== '') {
$params = explode('/', $strParams);
}
}
unset($parts['params']);
}
if ($params) {
$this->_params = array_merge($params, $parts);
} else {
$this->_params = $parts;
}
}
}
protected function loadLocalCache()
{
if (!is_file(static::$cacheFile)) {
return false;
}
try {
if ($routes = include static::$cacheFile) {
$this->_routes = unserialize($routes);
return true;
}
} // @codeCoverageIgnoreStart
catch (\Exception $e) {
Log::exception($e);
}
return false;
// @codeCoverageIgnoreEnd
}
protected function loadRoutes()
{
$this->_routes = [];
is_file($file = ROOT_PATH . '/app/routes.php') ? include $file : print('路由文件位置存在问题');
/*is_array($routes) ? $this->addRoutes($routes) : print("路由文件非数组形式,请更改");*/
}
/* public function prefix($prefix, array $routes, $filter = null)
{
$this->addRoutes($routes, $prefix, $filter);
return $this;
}*/
/*public function quickAdd($method, $uri, $handler, $filter = null)
{
$uri{0} == '/' or $uri = '/' . $uri;
if ($isArrayHandler = is_array($handler)) {
if (!$filter && isset($handler['filter'])) {
$filter = $handler['filter'];
unset($handler['filter']);
}
// Support for callable: ['Class', 'method']
if (isset($handler[0]) && isset($handler[1])) {
$handler['controller'] = $handler[0];
$handler['action'] = $handler[1];
unset($handler[0], $handler[1]);
}
empty($handler['controller']) and $handler = reset($handler);
}
if (is_string($handler)) {
list($controller, $action) = explode('::', $handler);
$handler = ['controller' => $controller, 'action' => $action];
} elseif ($handler instanceof Closure) {
$handler = ['controller' => new SerializableClosure($handler)];
} elseif ($isArrayHandler && isset($handler['controller']) && $handler['controller'] instanceof Closure) {
$handler['controller'] = new SerializableClosure($handler['controller']);
}
$method == 'ANY' and $method = null;
$method == 'GET' and $method = ['GET', 'HEAD'];
$route = $this->add($uri, $handler, $method);
is_callable($filter) or $filter = null;
$filter and $route->beforeMatch($filter);
return $route;
}*/
public function quickAdd($pattern, $paths, $method, $filter = null)
{
$method == 'ANY' and $method = null;
$method == 'GET' and $method = ['GET', 'HEAD'];
$route = $this->add($pattern, $paths, $method);
is_callable($filter) or $filter = null;
$filter and $route->beforeMatch($filter);
return $route;
}
public function CquickAdd($pattern, $function, $method, $filter = null)
{
$method == 'ANY' and $method = null;
$method == 'GET' and $method = ['GET', 'HEAD'];
$handler['controller'] = new SerializableClosure($function);
$route = $this->add($pattern, $handler, $method);
is_callable($filter) or $filter = null;
$filter and $route->beforeMatch($filter);
return $route;
}
/*public function CquickAdd($function, $method, $filter = null)
{
$method == 'ANY' and $method = null;
$method == 'GET' and $method = ['GET', 'HEAD'];
is_callable($filter) or $filter = null;
$filter and $route->beforeMatch($filter);
return $route;
}*/
public static function register(Di $di)
{
static::$di = $di;
static::$cacheFile = storagePath('cache/routes.php');
$di->remove('router');
$di->setShared('router', function () {
return new static();
});
}
public function reset()
{
static::$disableSession = false;
static::$disableCsrfCheck = false;
static::$currentUri = null;
$this->cookies->reset();
$this->response->setContent('')
->resetHeaders()
->setStatusCode(200);
}
protected function saveLocalCache()
{
fileSaveArray(static::$cacheFile, serialize($this->_routes));
}
protected function splitRoutes()
{
$exactRoutes = [];
$regexRoutes = [];
/* @var Route[] $routes */
$routes = array_reverse($this->_routes);
foreach ($routes as $route) {
$pattern = $route->getCompiledPattern();
$methods = (array)$route->getHttpMethods();
$methods or $methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'];
if ($pattern{0} === '#') {
foreach ($methods as $method) {
isset($regexRoutes[$method][$pattern]) or $regexRoutes[$method][$pattern] = $route;
}
} else {
foreach ($methods as $method) {
isset($exactRoutes[$method][$pattern]) or $exactRoutes[$method][$pattern] = $route;
}
}
}
$this->exactRoutes = $exactRoutes;
$this->regexRoutes = $regexRoutes;
}
public static function staticReset()
{
static::$router === null and static::$router = static::$di->getShared('router');
static::$router->reset();
}
/**
* @param string $content
* @param string $contentType
* @throws NotFoundException
*/
public static function throw404Exception($content = null, $contentType = 'text/html')
{
/*!$content && static::$runningUnitTest and $content = '404 NOT FOUND';
$content or $content = static::generateErrorPage('404', '404 NOT FOUND');*/
empty($content) and $content = '404 NOT FOUND';
throw new NotFoundException($content, ['content-type' => $contentType]);
}
/**
* @param string $content
* @param string $contentType
* @throws CsrfException
*/
public static function throwCsrfException($content = null, $contentType = 'text/html')
{
/*!$content && static::$runningUnitTest and $content = '403 FORBIDDEN';
$content or $content = static::generateErrorPage('csrf', '403 FORBIDDEN');*/
empty($content) and $content = '403 FORBIDDEN';
throw new CsrfException($content, ['content-type' => $contentType]);
}
/**
* @codeCoverageIgnore
* @param bool $flag
* @return bool
*/
public static function useLiteHandler($flag = null)
{
$flag === null or static::$useLiteHandler = (bool)$flag;
return static::$useLiteHandler;
}
/**
* 异常响应
* @param Response $response response对象
* @param null $message 内容
* @param int $code 异常代码
* @param array $headers header列表
* @return Response
* @author wangyu <wangyu@ledouya.com>
* @createTime 2018/5/19 15:42
*/
public static function exceptionResponse(Response $response, $message = null, $code = 500, $headers = [])
{
$response->setStatusCode($code);
$headers = [
'content-type: application/vnd.api+json',
'exception-type: HttpException',
];
foreach ($headers as $name => $value) {
if (is_numeric($name)) {
list($name, $value) = explode(':', $value);
}
$response->setHeader(trim($name), trim($value));
}
$body = json_encode([
'jsonapi' => ['version' => '1.0'],
'error' => $code,
'error_reason' => empty($message) ? 'Server down, please check log!' : $message
]);
$response->setContent($body); //设置响应内容
return $response;
}
}
<?php
namespace Phwoolcon\Router\Filter;
use Phalcon\Mvc\Router\Route;
use Phwoolcon\Router;
use Phwoolcon\Router\FilterInterface;
use Phwoolcon\Router\FilterTrait;
class DisableCsrfFilter implements FilterInterface
{
use FilterTrait;
/**
* @param string $uri
* @param Route $route
* @param Router $router
* @return bool
*/
protected function filter($uri, $route, $router)
{
Router::disableCsrfCheck();
return true;
}
}
<?php
namespace Phwoolcon\Router\Filter;
use Phalcon\Mvc\Router\Route;
use Phwoolcon\Router;
use Phwoolcon\Router\FilterInterface;
use Phwoolcon\Router\FilterTrait;
class DisableSessionFilter implements FilterInterface
{
use FilterTrait;
/**
* @param string $uri
* @param Route $route
* @param Router $router
* @return bool
*/
protected function filter($uri, $route, $router)
{
Router::disableSession();
return true;
}
}
<?php
namespace Phwoolcon\Router\Filter;
use Phalcon\Mvc\Router\Route;
use Phwoolcon\Router;
use Phwoolcon\Router\FilterInterface;
use Phwoolcon\Router\FilterTrait;
class MultiFilter implements FilterInterface
{
/**
* @var FilterInterface[]
*/
protected $filters = [];
use FilterTrait;
public function __invoke($uri, $route, $router)
{
return $this->filter($uri, $route, $router);
}
public function add(FilterInterface $filter)
{
$this->filters[get_class($filter)] = $filter;
return $this;
}
/**
* @param string $uri
* @param Route $route
* @param Router $router
* @return bool
*/
protected function filter($uri, $route, $router)
{
foreach ($this->filters as $filter) {
if (!$filter->__invoke($uri, $route, $router)) {
return false;
}
}
return true;
}
public static function instance()
{
return new static;
}
/**
* @param string $key
* @return $this
* @codeCoverageIgnore
*/
public function remove($key)
{
unset($this->filters[$key]);
return $this;
}
}
<?php
namespace Phwoolcon\Router;
use Phalcon\Mvc\Router\Route;
use Phwoolcon\Router;
interface FilterInterface
{
/**
* @param string $uri
* @param Route $route
* @param Router $router
* @return bool
*/
public function __invoke($uri, $route, $router);
}
<?php
namespace Phwoolcon\Router;
use Phalcon\Mvc\Router\Route;
use Phwoolcon\Router;
trait FilterTrait
{
/**
* @var static
*/
protected static $instance;
/**
* @param string $uri
* @param Route $route
* @param Router $router
* @return bool
*/
abstract protected function filter($uri, $route, $router);
/**
* @param string $uri
* @param Route $route
* @param Router $router
* @return bool
*/
public function __invoke($uri, $route, $router)
{
static::$instance or static::instance();
return static::$instance->filter($uri, $route, $router);
}
public static function instance()
{
static::$instance or static::$instance = new static;
return static::$instance;
}
}
<?php
namespace Phwoolcon;
use Phalcon\Security as PhalconSecurity;
class Security extends PhalconSecurity
{
public static function prepareSignatureData(array $data)
{
ksort($data);
unset($data['sign']);
$string = [];
foreach ($data as $k => $v) {
$string[] = $k . '=' . $v;
}
return implode('&', $string);
}
public static function sha256($data, $raw = false)
{
return hash('sha256', $data, $raw);
}
public static function signArrayMd5(array $data, $secret)
{
return md5(md5(static::prepareSignatureData($data)) . $secret);
}
public static function signArraySha256(array $data, $secret)
{
return static::sha256(static::sha256(static::prepareSignatureData($data)) . $secret);
}
public static function signArrayHmacSha256(array $data, $secret)
{
return hash_hmac('sha256', static::prepareSignatureData($data), $secret);
}
}
<?php
namespace Phwoolcon;
use Phalcon\Di;
use Phalcon\Session\Adapter;
use Phwoolcon\Exception\InvalidConfigException;
use Phwoolcon\Session\AdapterInterface;
use Phwoolcon\Session\AdapterTrait;
/**
* Class Session
* @package Phwoolcon
*
* @method static void clear()
* @uses AdapterTrait::clear()
* @method static void clearFormData(string $key)
* @uses AdapterTrait::clearFormData()
* @method static bool destroy(bool $removeData = false)
* @uses \Phalcon\Session\Adapter::destroy()
* @method static void end()
* @uses AdapterTrait::end()
* @method static void flush()
* @uses AdapterTrait::flush()
* @method static mixed get(string $index, mixed $defaultValue = null, bool $remove = false)
* @uses AdapterTrait::get()
* @method static mixed getFormData(string $key, mixed $default = null)
* @uses AdapterTrait::getFormData()
* @method static string generateCsrfToken()
* @uses AdapterTrait::generateCsrfToken()
* @method static string generateRandomString()
* @uses AdapterTrait::generateRandomString()
* @method static string getCsrfToken(bool $renew = false)
* @uses AdapterTrait::getCsrfToken()
* @method static string getId()
* @uses \Phalcon\Session\Adapter::getId()
* @method static string getName()
* @uses \Phalcon\Session\Adapter::getName()
* @method static array getOptions()
* @uses \Phalcon\Session\Adapter::getOptions()
* @method static bool has(string $index)
* @uses \Phalcon\Session\Adapter::has()
* @method static bool isStarted()
* @uses \Phalcon\Session\Adapter::isStarted()
* @method static AdapterTrait regenerateId(bool $deleteOldSession = true)
* @uses AdapterTrait::regenerateId()
* @method static void rememberFormData(string $key, $data)
* @uses AdapterTrait::rememberFormData()
* @method static void remove(string $index)
* @uses AdapterTrait::remove()
* @method static void set(string $index, mixed $value)
* @uses AdapterTrait::set()
* @method static AdapterTrait setCookie()
* @uses AdapterTrait::setCookie()
* @method static void setId(string $id)
* @uses \Phalcon\Session\Adapter::setId()
* @method static void setName(string $id)
* @uses \Phalcon\Session\Adapter::setName()
* @method static void setOptions(array $options)
* @uses \Phalcon\Session\Adapter::setOptions()
* @method static bool start()
* @uses AdapterTrait::start()
* @method static int status()
* @uses \Phalcon\Session\Adapter::status()
*/
class Session
{
/**
* @var Di
*/
protected static $di;
/**
* @var Adapter|AdapterTrait
*/
static protected $session;
public static function __callStatic($name, $arguments)
{
static::$session or static::$session = static::$di->getShared('session');
return call_user_func_array([static::$session, $name], $arguments);
}
public static function register(Di $di)
{
static::$di = $di;
$di->remove('session');
static::$session = null;
$di->setShared('session', function () {
$default = Config::get('session.default');
$config = Config::get('session.drivers.' . $default);
$class = $config['adapter'];
$options = $config['options'];
$options += Config::get('session.options');
$options['cookies'] += Config::get('cookies');
session_name($options['cookies']['name']);
// @codeCoverageIgnoreStart
if (!$class || !class_exists($class)) {
$errorMessage = "Invalid session adapter {$class}, please check config file session.php";
throw new InvalidConfigException($errorMessage);
}
// @codeCoverageIgnoreEnd
$session = new $class($options);
// @codeCoverageIgnoreStart
if (!$session instanceof AdapterInterface) {
$errorMessage = "Session adapter {$class} should implement " . AdapterInterface::class;
throw new InvalidConfigException($errorMessage);
}
// @codeCoverageIgnoreEnd
return $session;
});
}
}
<?php
namespace Phwoolcon\Session\Adapter;
use Phalcon\Session\Adapter\Libmemcached;
use Phwoolcon\Config;
use Phwoolcon\Session\AdapterInterface;
use Phwoolcon\Session\AdapterTrait;
/**
* Class Memcached
* @package Phwoolcon\Session\Adapter
*
* @property \Phalcon\Cache\Backend\Libmemcached $_libmemcached
* @method \Phalcon\Cache\Backend\Libmemcached getLibmemcached()
*/
class Memcached extends Libmemcached implements AdapterInterface
{
use AdapterTrait;
public function __construct(array $options = [])
{
$options = array_merge(Config::get('cache.drivers.memcached.options'), $options);
parent::__construct($options);
}
public function flush()
{
$this->_libmemcached->flush();
$this->_libmemcached->delete('_PHCM');
}
}
<?php
namespace Phwoolcon\Session\Adapter;
use Phalcon\Session\Adapter\Files;
use Phwoolcon\Config;
use Phwoolcon\Session\AdapterInterface;
use Phwoolcon\Session\AdapterTrait;
class Native extends Files implements AdapterInterface
{
use AdapterTrait;
public function __construct($options = null)
{
parent::__construct($options);
$sessionPath = $options['save_path'];
is_dir($sessionPath) or mkdir($sessionPath, 0755, true);
session_save_path($sessionPath);
}
public function flush()
{
foreach (glob($this->_options['save_path'] . '/sess*') as $file) {
@unlink($file);
}
}
}
<?php
namespace Phwoolcon\Session\Adapter;
use Phalcon\Cache\Frontend\None as FrontendNone;
use Phalcon\Session\Adapter\Redis as RedisSession;
use Phwoolcon\Cache\Backend\Redis as RedisCache;
use Phwoolcon\Config;
use Phwoolcon\Session\AdapterInterface;
use Phwoolcon\Session\AdapterTrait;
/**
* Class Redis
* @package Phwoolcon\Session\Adapter
*
* @property RedisCache $_redis
* @method RedisCache getRedis()
*/
class Redis extends RedisSession implements AdapterInterface
{
use AdapterTrait;
public function __construct(array $options = [])
{
$options = array_merge(Config::get('cache.drivers.redis.options'), $options);
parent::__construct($options);
$this->_redis = new RedisCache(new FrontendNone, $options);
}
public function flush()
{
$this->_redis->flush();
}
}
<?php
namespace Phwoolcon\Session;
interface AdapterInterface
{
public function end();
public function readCookieAndStart();
public function setCookie();
}
<?php
namespace Phwoolcon\Session;
use Phwoolcon\Cookies;
use Phwoolcon\Text;
/**
* Class AdapterTrait
* @package Phwoolcon\Session
*
* @property bool $_options
* @property bool $_started
* @property string $_uniqueId
*/
trait AdapterTrait
{
protected $cookieRenewedAt = 0;
public function clear()
{
$this->_started or $this->start();
$this->cookieRenewedAt = 0;
$_SESSION = [];
}
public function clearFormData($key)
{
$this->remove('form_data.' . $key);
}
public function end()
{
if ($this->_started) {
$this->setCookie();
session_write_close();
session_unset();
unset($_SESSION);
}
$this->_started = false;
$this->cookieRenewedAt = 0;
}
abstract public function flush();
public function generateCsrfToken()
{
$this->set('csrf_token', $token = Text::token());
$this->set('csrf_token_expire', time() + $this->_options['csrf_token_lifetime']);
return $token;
}
/**
* @param string $index
* @param mixed $defaultValue
* @param bool $remove
* @return mixed
*/
public function get($index, $defaultValue = null, $remove = false)
{
$this->_started or $this->start();
$this->_uniqueId && $index = $this->_uniqueId . '#' . $index;
$value = fnGet($_SESSION, $index, $defaultValue, '.');
$remove && array_forget($_SESSION, $index);
return $value;
}
public function getCsrfToken($renew = false)
{
$result = ($this->get('csrf_token_expire') > ($now = time()) && $token = $this->get('csrf_token')) ?
$token :
$this->generateCsrfToken();
$renew && $this->set('csrf_token_expire', $now + $this->_options['csrf_token_lifetime']);
return $result;
}
public function getFormData($key, $default = null)
{
return $this->get('form_data.' . $key, $default);
}
protected function isValidSid($value)
{
return isset($value{31}) && !isset($value{32}) && ctype_xdigit($value);
}
public function readCookieAndStart()
{
if (!$this->_started && $this->status() !== self::SESSION_ACTIVE) {
$this->isValidSid($sid = Cookies::get($this->getName())->useEncryption(false)->getValue()) ?
$this->setId($sid) : $this->regenerateId();
session_start();
$this->_options['cookie_lazy_renew_interval'] && $this->cookieRenewedAt = $this->get('_cookie_renewed_at');
$this->_started = true;
return true;
}
return false;
}
public function regenerateId($deleteOldSession = true)
{
$this->setId(Text::token());
$this->cookieRenewedAt = 0;
return $this;
}
public function rememberFormData($key, $data)
{
$this->set('form_data.' . $key, $data);
}
/**
* @param string $index
*/
public function remove($index)
{
$this->_started or $this->start();
$this->_uniqueId && $index = $this->_uniqueId . '#' . $index;
array_forget($_SESSION, $index);
}
/**
* @param string $index
* @param mixed $value
*/
public function set($index, $value)
{
$this->_started or $this->start();
$this->_uniqueId && $index = $this->_uniqueId . '#' . $index;
array_set($_SESSION, $index, $value);
}
public function setCookie()
{
$now = time();
if ($lazyRenew = $this->_options['cookie_lazy_renew_interval']) {
if ($this->cookieRenewedAt + $lazyRenew > $now) {
return $this;
}
$this->set('_cookie_renewed_at', $this->cookieRenewedAt = $now);
}
Cookies::set(
$cookieName = $this->getName(),
$this->getId(),
$now + $this->_options['lifetime'],
$this->_options['cookies']['path'],
$this->_options['cookies']['secure'],
$this->_options['cookies']['domain'],
$this->_options['cookies']['http_only']
);
Cookies::get($cookieName)->useEncryption(false);
return $this;
}
public function start()
{
return $this->readCookieAndStart();
}
}
<?php
namespace Phwoolcon;
use Phalcon\Text as PhalconText;
class Text extends PhalconText
{
/**
* @param string $string
* @param int $length
* @param string $suffix
* @return string
* @codeCoverageIgnore
*/
public static function ellipsis($string, $length, $suffix = '...')
{
return mb_strlen($string) > $length ? mb_substr($string, 0, $length - mb_strlen($suffix)) . $suffix : $string;
}
public static function escapeHtml($string, $newLineToBr = true)
{
$result = htmlspecialchars($string);
return $newLineToBr ? nl2br($result) : $result;
}
/**
* Pad or truncate input string to fixed length
* <code>
* echo Phwoolcon\Text::padOrTruncate('123', '0', 4); // prints 0123
* echo Phwoolcon\Text::padOrTruncate('123456', '0', 4); // prints 3456
* </code>
*
* @param string $input
* @param string $padding
* @param int $length
* @return string
*/
public static function padOrTruncate($input, $padding, $length)
{
return isset($input{$length}) ? substr($input, -$length) : str_pad($input, $length, $padding, STR_PAD_LEFT);
}
public static function token()
{
return bin2hex(random_bytes(16));
}
/**
* @param int $type
* @param int $length
* @return string
* @codeCoverageIgnore
*/
public static function random($type = 0, $length = 8)
{
if ($type === static::RANDOM_ALNUM) {
return substr(str_replace(['+', '/'], '', base64_encode(random_bytes($length * 2))), 0, $length);
}
return parent::random($type, $length);
}
}
<?php
namespace Phwoolcon\Util;
use Phalcon\Di;
use Phwoolcon\Config;
use Phwoolcon\Util\Counter\AdapterInterface;
class Counter
{
/**
* @var AdapterInterface
*/
protected static $adapter;
/**
* @var Di
*/
protected static $di;
public static function increment($keyName, $value = 1)
{
static::$adapter === null and static::$adapter = static::$di->getShared('counter');
return static::$adapter->increment($keyName, ((int)$value) ?: 1);
}
public static function decrement($keyName, $value = 1)
{
static::$adapter === null and static::$adapter = static::$di->getShared('counter');
return static::$adapter->decrement($keyName, ((int)$value) ?: 1);
}
public static function reset($keyName)
{
static::$adapter === null and static::$adapter = static::$di->getShared('counter');
return static::$adapter->reset($keyName);
}
public static function register(Di $di)
{
static::$di = $di;
$di->remove('counter');
static::$adapter = null;
$di->setShared('counter', function () {
$default = Config::get('counter.default');
$config = Config::get('counter.drivers.' . $default);
$class = $config['adapter'];
$options = $config['options'];
strpos($class, '\\') === false and $class = 'Phwoolcon\\Util\\Counter\\' . $class;
return new $class($options);
});
}
}
<?php
namespace Phwoolcon\Util\Counter;
abstract class Adapter implements AdapterInterface
{
protected $options = [];
public function __construct($options)
{
$this->options = $options;
}
}
<?php
namespace Phwoolcon\Util\Counter\Adapter;
use Phwoolcon\Config;
use Phwoolcon\Util\Counter\Adapter;
use Phwoolcon\Util\Counter\AdapterInterface;
class Auto extends Adapter
{
/**
* @var AdapterInterface
*/
protected $realCounter;
protected $realOptions = [];
public function __construct($options)
{
parent::__construct($options);
$cacheType = Config::get('cache.default');
if ($cacheType == 'redis' || $cacheType == 'memcached') {
$realAdapter = Cache::class;
$this->realOptions = Config::get('counter.drivers.cache.options');
} // @codeCoverageIgnoreStart
else {
$realAdapter = Rds::class;
$this->realOptions = Config::get('counter.drivers.rds.options');
}
// @codeCoverageIgnoreEnd
$this->realCounter = new $realAdapter($this->realOptions);
}
public function increment($keyName, $value = 1)
{
return $this->realCounter->increment($keyName, $value);
}
public function decrement($keyName, $value = 1)
{
return $this->realCounter->decrement($keyName, $value);
}
public function reset($keyName)
{
return $this->realCounter->reset($keyName);
}
}
<?php
namespace Phwoolcon\Util\Counter\Adapter;
use Phalcon\Cache\Backend;
use Phalcon\Di;
use Phwoolcon\Cache\Backend\Redis;
use Phwoolcon\Util\Counter\Adapter;
class Cache extends Adapter
{
/**
* @var Backend|Redis
*/
protected $cache;
protected $prefix = 'counter:';
public function __construct($options)
{
parent::__construct($options);
$this->cache = Di::getDefault()->getShared('cache');
$this->cache instanceof Backend\File and $this->prefix = 'counter-';
}
public function increment($keyName, $value = 1)
{
return $this->cache->increment($this->prefix . $keyName, $value);
}
public function decrement($keyName, $value = 1)
{
return $this->cache->decrement($this->prefix . $keyName, $value);
}
public function reset($keyName)
{
$this->cache->save($this->prefix . $keyName, 0);
}
}
<?php
namespace Phwoolcon\Util\Counter\Adapter;
use Phalcon\Db\Column;
use Phalcon\Db\Dialect;
use Phalcon\Db\RawValue;
use Phwoolcon\Db;
use Phwoolcon\Util\Counter\Adapter;
class Rds extends Adapter
{
protected $table = 'counter';
protected $selectSql;
protected $updateCondition;
/**
* @var Dialect
*/
protected $dialect;
public function __construct($options)
{
parent::__construct($options);
isset($options['table']) and $this->table = $options['table'];
$db = Db::connection();
if (!$db->tableExists($this->table)) {
$this->createTable();
}
/* @var Dialect $dialect */
$dialect = $this->dialect = $db->getDialect();
$this->selectSql = $dialect->select([
'tables' => [$this->table],
'columns' => ['value'],
'where' => $where = $dialect->escape('key') . ' = ?',
'forUpdate' => true,
]);
$this->updateCondition = $where;
}
protected function createTable()
{
$db = Db::connection();
$db->createTable($this->table, null, [
'columns' => [
new Column('key', [
'type' => Column::TYPE_VARCHAR,
'size' => 255,
'notNull' => true,
'primary' => true,
]),
new Column('value', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => false,
'notNull' => true,
]),
],
'options' => [
'TABLE_COLLATION' => 'utf8_unicode_ci',
],
]);
}
public function increment($keyName, $value = 1)
{
$db = Db::connection();
$db->begin();
if (!$db->query($this->selectSql, [$keyName])->fetch()) {
$db->insert($this->table, [
'key' => $keyName,
'value' => $value,
]);
$result = $value;
} else {
$db->updateAsDict($this->table, [
'value' => new RawValue($this->dialect->escape('value') . ' + ' . (int)$value),
], [
'conditions' => $this->updateCondition,
'bind' => [$keyName],
]);
$result = $db->fetchColumn($this->selectSql, [$keyName]);
}
$db->commit();
return $result;
}
public function decrement($keyName, $value = 1)
{
return $this->increment($keyName, -$value);
}
public function reset($keyName)
{
$db = Db::connection();
$db->updateAsDict($this->table, [
'value' => 0,
], [
'conditions' => $this->updateCondition,
'bind' => [$keyName],
]);
}
}
<?php
namespace Phwoolcon\Util\Counter;
interface AdapterInterface
{
public function increment($keyName, $value = 1);
public function decrement($keyName, $value = 1);
public function reset($keyName);
}
<?php
namespace Phwoolcon\Util\Reflection\Stringify;
use ReflectionParameter;
class Parameter
{
public static function cast(ReflectionParameter $parameter)
{
$stringify = '';
// @codeCoverageIgnoreStart
if (PHP_VERSION_ID >= 70000 && $parameter->hasType()) {
$stringify .= $parameter->getType()->__toString() . ' ';
} else {
if ($parameter->isArray()) {
$stringify .= 'array ';
} elseif ($type = $parameter->getClass()) {
$stringify .= $type->getName() . ' ';
} elseif ($parameter->isCallable()) {
$stringify .= 'callable ';
}
}
// @codeCoverageIgnoreEnd
$parameter->isPassedByReference() and $stringify .= '&';
$stringify .= '$' . $parameter->getName();
if ($parameter->isOptional()) {
$default = var_export($parameter->getDefaultValue(), true);
if ($const = $parameter->getDefaultValueConstantName()) {
$const = defined($const) ? $const : substr(strrchr($const, '\\'), 1);
defined($const) and $default = $const;
}
$default == 'NULL' and $default = 'null';
$stringify .= ' = ' . $default;
}
return $stringify;
}
}
<?php
namespace Phwoolcon\Util;
class Timer
{
protected static $_times;
public static function start()
{
return static::$_times[0] = explode(' ', microtime());
}
public static function stop()
{
static::$_times[1] = explode(' ', microtime());
return (static::$_times[1][0] - static::$_times[0][0]) + (static::$_times[1][1] - static::$_times[0][1]);
}
}
<?php
namespace Phwoolcon;
use Exception;
use Phalcon\Assets\Filters\Cssmin;
use Phalcon\Assets\Filters\Jsmin;
use Phalcon\Assets\Manager;
use Phalcon\Cache\BackendInterface;
use Phalcon\Di;
use Phalcon\Http\Response;
use Phalcon\Mvc\View as PhalconView;
use Phalcon\Mvc\View\Exception as ViewException;
use Phwoolcon\Assets\Resource\Css;
use Phwoolcon\Assets\Resource\Js;
use Phwoolcon\Assets\ResourceTrait;
use Phwoolcon\Daemon\ServiceAwareInterface;
class View extends PhalconView implements ServiceAwareInterface
{
/**
* @var Di
*/
protected static $di;
/**
* @var static
*/
protected static $instance;
protected static $cachedAssets = [];
protected static $runningUnitTest = false;
protected $config = [];
protected $_theme;
protected $_defaultTheme;
protected $_loadedThemes = [];
protected $_viewsDir;
protected $_assetsCdnPrefix = '';
protected $_fillResponse = true;
protected $_templateExtensions = [];
/**
* @var Manager
*/
public $assets;
/**
* @var Response
*/
public $response;
public function __construct($config = null)
{
parent::__construct($config['options']);
$this->response = static::$di->getShared('response');
$this->setViewsDir($this->_viewsDir = $config['path']);
$this->_mainView = $config['top_level'];
$this->_theme = $config['theme'];
$this->_defaultTheme = 'default';
$this->_layout = $config['default_layout'];
$this->config = $config;
$this->registerEngines($config['engines']);
$this->_templateExtensions = array_keys($config['engines']);
$this->_assetsCdnPrefix = trim(url($config['options']['assets_options']['cdn_prefix']), '/');
$basePath = $this->config['options']['assets_options']['base_path'];
ResourceTrait::setBasePath($basePath);
ResourceTrait::setRunningUnitTests(static::$runningUnitTest);
//禁用视图渲染级别
$this->disableLevel([
View::LEVEL_LAYOUT => true,
View::LEVEL_MAIN_LAYOUT => true,
]);
}
protected function _engineRender($engines, $viewPath, $silence, $mustClean, BackendInterface $cache = null)
{
$viewPath = trim($viewPath, '/');
$silence = $silence && !$this->config['debug'];
$this->_options['debug_wrapper'] = $this->config['debug'] ?
($viewPath == $this->_mainView ? null : $this->getDebugWrapper($viewPath)) : null;
$viewPath == $this->_mainView or $viewPath = $this->getAbsoluteViewPath($viewPath);
parent::_engineRender($engines, $viewPath, $silence, $mustClean, $cache);
}
/**
* @param string $collectionName
* @return string
*/
public static function assets($collectionName)
{
static::$instance or static::$instance = static::$di->getShared('view');
$view = static::$instance;
$useCache = $view->config['options']['assets_options']['cache_assets'];
if (!$view->assets) {
$view->assets = static::$di->getShared('assets');
$useCache and static::$cachedAssets = Cache::get('assets');
}
$type = substr($collectionName, strrpos($collectionName, '-') + 1);
$view->isAdmin() and $collectionName = 'admin-' . $collectionName;
if ($useCache && isset(static::$cachedAssets[$collectionName])) {
return static::$cachedAssets[$collectionName];
}
$view->loadAssets($view->config['assets']);
$view->loadAssets($view->config['admin']['assets'], true);
ob_start();
try {
switch ($type) {
case 'js':
$view->assets->outputJs($collectionName);
break;
case 'css':
$view->assets->outputCss($collectionName);
break;
}
} // @codeCoverageIgnoreStart
catch (Exception $e) {
Log::exception($e);
}
// @codeCoverageIgnoreEnd
$assets = ob_get_clean();
if ($view->config['options']['assets_options']['apply_filter']) {
$assets = str_replace(['http://', 'https://'], '//', $assets);
}
static::$cachedAssets[$collectionName] = $assets;
$useCache and Cache::set('assets', static::$cachedAssets);
return $assets;
}
public static function clearAssetsCache()
{
static::$cachedAssets = [];
Cache::delete('assets');
}
public static function generateBodyJs()
{
return static::assets('body-js');
}
public static function generateHeadCss()
{
return static::assets('head-css');
}
public static function generateHeadJs()
{
return static::assets('head-js');
}
public static function generateIeHack()
{
return static::assets('ie-hack-css') . static::assets('ie-hack-js');
}
public static function generateIeHackBodyJs()
{
return static::assets('ie-hack-body-js');
}
/**
* @param string $view
* @return string
*/
public function getAbsoluteViewPath($view)
{
$path = $this->_viewsDir . $this->_theme . '/' . $view;
if (is_file($path)) {
return $path;
}
foreach ($this->_registeredEngines as $ext => $engine) {
if (is_file($path . $ext)) {
return $path;
}
}
return $this->_viewsDir . $this->_defaultTheme . '/' . $view;
}
/**
* @param string $key
* @return mixed
*/
public static function getConfig($key = null)
{
static::$instance or static::$instance = static::$di->getShared('view');
return fnGet(static::$instance->config, $key);
}
/**
* @return mixed
* @codeCoverageIgnore
*/
public function getCurrentTheme()
{
return $this->_theme;
}
/**
* @param $viewPath
* @return array
* @codeCoverageIgnore
*/
public function getDebugWrapper($viewPath)
{
$viewPath = trim($this->_theme . '/' . $viewPath, '/');
return ["<!-- [{$viewPath} -->\n", "<!-- {$viewPath}] -->\n"];
}
public static function getPageDescription()
{
return static::getParam('page_description');
}
public static function getPageKeywords()
{
$keywords = static::getParam('page_keywords');
is_array($keywords) and $keywords = implode(',', $keywords);
return $keywords;
}
public static function getPageLanguage()
{
return strtr(static::getParam('page_language', I18n::getCurrentLocale()), ['_' => '-']);
}
public static function getPageTitle()
{
$title = static::getParam('page_title');
is_array($title) and $title = implode(static::getConfig('title_separator'), array_reverse($title));
return $title;
}
/**
* @param string $key
* @param mixed $default
* @return mixed
*/
public static function getParam($key, $default = null)
{
static::$instance or static::$instance = static::$di->getShared('view');
return fnGet(static::$instance->_viewParams, $key, $default, '.');
}
public static function getPhwoolconJsOptions()
{
static::$instance or static::$instance = static::$di->getShared('view');
$options = Events::fire('view:generatePhwoolconJsOptions', static::$instance, [
'baseUrl' => url(''),
'debug' => static::$instance->config['debug'],
]) ?: [];
return $options;
}
public function isAdmin($flag = null)
{
if ($flag !== null) {
$this->_options['is_admin'] = $flag = (bool)$flag;
$this->_theme = $flag ? 'admin/' . $this->config['admin']['theme'] : $this->config['theme'];
$this->_defaultTheme = $flag ? 'admin/default' : 'default';
}
return !empty($this->_options['is_admin']);
}
public function loadAssets($assets, $isAdmin = false)
{
$prefix = $isAdmin ? 'admin-' : '';
if (isset($this->_loadedThemes[$prefix])) {
return $this;
}
$this->_loadedThemes[$prefix] = true;
$assetsOptions = $this->config['options']['assets_options'];
$applyFilter = $assetsOptions['apply_filter'];
// The base path, usually the public directory
$basePath = $assetsOptions['base_path'];
// The assets dir inside base path
$resourcePath = '/' . $assetsOptions['assets_dir'] . '/';
$compiledPath = '/' . $assetsOptions['compiled_dir'] . '/';
foreach ($assets as $collectionName => $resources) {
ksort($resources, SORT_NATURAL);
$resourceType = substr($collectionName, strrpos($collectionName, '-') + 1);
$collectionName = $prefix . $collectionName;
$collection = $this->assets->collection($collectionName);
$collection->setSourcePath($basePath);
$contentHash = '';
switch ($resourceType) {
case 'css':
foreach ($resources as $item) {
$isLocal = !isHttpUrl($item);
$resource = new Css($isLocal ? $resourcePath . $item : $item, $isLocal);
$applyFilter and $contentHash = $resource->concatenateHash($contentHash);
$collection->add($resource);
}
$applyFilter and $collection->addFilter(new Cssmin());
break;
case 'js':
foreach ($resources as $item) {
$isLocal = !isHttpUrl($item);
$resource = new Js($isLocal ? $resourcePath . $item : $item, $isLocal);
$applyFilter and $contentHash = $resource->concatenateHash($contentHash);
$collection->add($resource);
}
$applyFilter and $collection->addFilter(new Jsmin());
break;
}
$targetUri = $compiledPath . substr($collectionName, 0, -1 - strlen($resourceType));
$contentHash and $targetUri .= '.' . $contentHash;
$targetUri .= '.' . $resourceType;
$collection->setTargetPath($basePath . $targetUri)
->setTargetUri($this->_assetsCdnPrefix . $targetUri);
}
return $this;
}
/**
* `View::make(string $path[, string $file[, array $params]])`
*
* Or use two-parameter invocation: @since v1.1.6
* `View::make(string $path[, array $params])`
*
* @param string $path
* @param string|array $file
* @param array $params
* @return string
*/
public static function make($path, $file = '', $params = null)
{
static::$instance or static::$instance = static::$di->getShared('view');
return static::$instance->render($path, $file, $params)->getContent();
}
public static function noFooter($flag = null)
{
static::$instance or static::$instance = static::$di->getShared('view');
$flag === null or static::$instance->_options['no_footer'] = (bool)$flag;
return !empty(static::$instance->_options['no_footer']);
}
public static function noHeader($flag = null)
{
static::$instance or static::$instance = static::$di->getShared('view');
$flag === null or static::$instance->_options['no_header'] = (bool)$flag;
return !empty(static::$instance->_options['no_header']);
}
public static function register(Di $di)
{
static::$di = $di;
static::$runningUnitTest = Config::runningUnitTest();
$di->setShared('view', function () {
return new static(Config::get('view'));
});
}
/**
* Use like Phalcon:
* `View::render(string $controllerName[, string $actionName[, array $params]])`
* ```php
* $this->render($controllerName, $actionName, ['some_var' => 'var_value']);
* ```
*
* Or use two-parameter invocation: @since v1.1.6
* `View::render(string $path[, array $params])`
*
* ```php
* $this->render($path, ['some_var' => 'var_value']);
* ```
*
* @param string $controllerName The controller name, or the template path (two-parameter invocation)
* @param string|array $actionName The action name, or the parameters (two-parameter invocation)
* @param array $params
* @return bool|PhalconView
* @throws Exception
*/
public function render($controllerName, $actionName = '', $params = null)
{
try {
if (is_array($actionName)) {
$params = $params ? $actionName + $params : $actionName;
$actionName = '';
}
/**
* 1. Breaking change in phalcon 3.2.3:
*
* @see https://github.com/phalcon/cphalcon/commit/3f703832786c7fb7a420bcf31ea0953ba538591d
*
* 2. Set `$this->_viewParams` directly to avoid error:
* `First argument is not an array` error in `parent::setVars()`
*/
$params and $this->_viewParams = $params;
$this->start();
$result = parent::render($controllerName,$actionName);
// die(parent::getControllerName().parent::getActionName().json_encode(parent::getViewsDir()));
$this->finish();
$this->_fillResponse and $this->response->setContent($this->getContent());
return $result;
} // @codeCoverageIgnoreStart
catch (ViewException $e) {
Log::exception($e);
return false;
} catch (Exception $e) {
throw $e;
}
// @codeCoverageIgnoreEnd
}
public function reset()
{
$this->_disabled = false;
$this->_options = $this->config['options'];
$this->_theme = $this->config['theme'];
$this->_defaultTheme = 'default';
$this->_layout = $this->config['default_layout'];
$this->_cache = null;
$this->_renderLevel = static::LEVEL_MAIN_LAYOUT;
$this->_cacheLevel = static::LEVEL_NO_RENDER;
$this->_content = null;
$this->_templatesBefore = [];
$this->_templatesAfter = [];
$this->_viewParams = [];
$this->_mainView = $this->config['top_level'];
$this->_fillResponse = true;
return $this;
}
public function setContent($content)
{
// @codeCoverageIgnoreStart
if (isset($this->_options['debug_wrapper'])) {
$wrapper = $this->_options['debug_wrapper'];
$this->_content = $wrapper[0] . $content . $wrapper[1];
} // @codeCoverageIgnoreEnd
else {
$this->_content = $content;
}
return $this;
}
public function setParams(array $params)
{
$this->_viewParams = $params;
}
}
<?php
namespace Phwoolcon\View\Engine;
use Phalcon\Mvc\View\Engine\Php as PhpEngine;
use Phalcon\Mvc\View\Exception;
use Phwoolcon\View;
/**
* Class Php
* @package Phwoolcon\View\Engine
*
* @property View $_view
* @method void include(string $path, $params = [])
* @uses \Phwoolcon\View\Engine\Php::processInclude()
*/
class Php extends PhpEngine
{
protected $_debug;
/**
* Php constructor.
* @param View $view
* @param \Phalcon\Di $di
*/
public function __construct($view, $di)
{
parent::__construct($view, $di);
$this->_debug = $view::getConfig('debug');
}
public function __call($name, $params)
{
call_user_func_array([$this, 'process' . ucfirst($name)], $params);
}
public function processInclude($path, $params = [])
{
if (!is_file($fullPath = $this->_view->getAbsoluteViewPath($path . '.phtml'))) {
// @codeCoverageIgnoreStart
if ($this->_debug) {
throw new Exception("View file '{$fullPath}' was not found");
}
return;
// @codeCoverageIgnoreEnd
}
// @codeCoverageIgnoreStart
if ($this->_debug) {
$wrapper = $this->_view->getDebugWrapper($path);
echo $wrapper[0];
$this->render($fullPath, $params);
echo $wrapper[1];
return;
}
// @codeCoverageIgnoreEnd
$this->render($fullPath, $params);
}
}
<?php
namespace Phwoolcon\View;
use Closure;
use Phalcon\Tag;
use Phwoolcon\Exception\WidgetException;
use Phwoolcon\Session;
use Phwoolcon\Util\Reflection\Stringify\Parameter;
use ReflectionFunction;
use ReflectionMethod;
/**
* Class Widget
*
* @package Phwoolcon\View
*
* @method static string csrfTokenField()
* @uses Widget::builtInCsrfTokenField()
* @method static string label(array $parameters, string $innerHtml)
* @uses Widget::builtInLabel()
* @method static string multipleChoose(array $parameters)
* @uses Widget::builtInMultipleChoose()
* @method static string singleChoose(array $parameters)
* @uses Widget::builtInSingleChoose()
*/
class Widget
{
protected static $builtInWidgets = [];
/**
* @var callable[]
*/
protected static $widgets = [];
public static function define($name, callable $definition)
{
static::$widgets[$name] = $definition;
}
public static function __callStatic($name, $arguments)
{
if (isset(static::$widgets[$name])) {
return call_user_func_array(static::$widgets[$name], $arguments);
} elseif (isset(static::$builtInWidgets[$name])) {
return call_user_func_array(static::$builtInWidgets[$name], $arguments);
} elseif (method_exists(static::class, $method = 'builtIn' . $name)) {
static::$builtInWidgets[$name] = $widget = [static::class, $method];
return call_user_func_array($widget, $arguments);
} else {
throw new WidgetException(__('Undefined widget "%name%"', ['name' => $name]));
}
}
protected static function builtInCsrfTokenField()
{
$token = Session::getCsrfToken(true);
return <<<TAG
<input type="hidden" name="_token" value="{$token}" autocomplete="off">
TAG;
}
/**
* Make a label element
*
* Required parameters:
* $innerHtml
* Optional parameters:
* 'for', 'class', or other html attributes
*
* @param array $parameters
* @param string $innerHtml
* @return string
*/
protected static function builtInLabel(array $parameters, $innerHtml)
{
return Tag::tagHtml('label', $parameters, false, true) . $innerHtml . Tag::tagHtmlClose('label');
}
/**
* Make a multiple select widget, if options < 5 it will be expanded into radios by default
*
* Required parameters:
* 'id', 'options' (in array)
* Optional parameters:
* 'name', 'class', 'value' (selected value) or other html attributes
* 'expand' (true or false, by default 'auto')
* 'useEmpty', 'emptyText' (used in select mode)
* 'prefix', 'suffix' (used to wrap radios in expanded mode)
* 'labelOn' ('left' or 'right', by default 'right', used to identify radios in expanded mode)
*
* @param array $parameters
* @return string
*/
protected static function builtInMultipleChoose(array $parameters)
{
static::checkRequiredParameters($parameters, ['id', 'options']);
$options = (array)$parameters['options'];
$parameters[0] = $id = $parameters['id'];
$expand = isset($parameters['expand']) ? $parameters['expand'] : 'auto';
unset($parameters['options'], $parameters['expand']);
// Expand select into radio buttons
if ($expand === true || ($expand == 'auto' && count($options) < 5)) {
$html = [];
$i = 0;
$radioParams = $parameters;
$labelOn = isset($radioParams['labelOn']) ? $radioParams['labelOn'] : 'right';
$prefix = isset($radioParams['prefix']) ? $radioParams['prefix'] : '';
$suffix = isset($radioParams['suffix']) ? $radioParams['suffix'] : '';
unset($radioParams['expand'], $radioParams['labelOn'], $radioParams['prefix'], $radioParams['suffix']);
unset($radioParams['options'], $radioParams['useEmpty'], $radioParams['emptyText']);
$selected = isset($parameters['value']) ? array_flip((array)$parameters['value']) : [];
foreach ($options as $value => $label) {
$radioParams['id'] = $radioId = $id . '_' . $i;
$radioParams['value'] = $value;
if (isset($selected[(string)$value])) {
$radioParams['checked'] = 'checked';
} else {
$radioParams['checked'] = null;
}
$checkbox = Tag::checkField($radioParams);
if ($labelOn == 'right') {
$checkbox .= PHP_EOL . $label;
} else {
$checkbox = $label . PHP_EOL . $checkbox;
}
$checkbox = static::label(['for' => $radioId], $checkbox);
++$i;
$html[] = $prefix . $checkbox . $suffix;
}
return implode(PHP_EOL, $html);
}
$parameters['multiple'] = true;
return Tag::select($parameters, $options);
}
/**
* Make a single select widget, if options < 5 it will be expanded into radios by default
*
* Required parameters:
* 'id', 'options' (in array)
* Optional parameters:
* 'name', 'class', 'value' (selected value) or other html attributes
* 'expand' (true or false, by default 'auto')
* 'useEmpty', 'emptyText' (used in select mode)
* 'prefix', 'suffix' (used to wrap radios in expanded mode)
* 'labelOn' ('left' or 'right', by default 'right', used to identify radios in expanded mode)
*
* @param array $parameters
* @return string
*/
protected static function builtInSingleChoose(array $parameters)
{
static::checkRequiredParameters($parameters, ['id', 'options']);
$options = (array)$parameters['options'];
$parameters[0] = $id = $parameters['id'];
$expand = isset($parameters['expand']) ? $parameters['expand'] : 'auto';
unset($parameters['options'], $parameters['expand']);
// Expand select into radio buttons
if ($expand === true || ($expand == 'auto' && count($options) < 5)) {
$html = [];
$i = 0;
$radioParams = $parameters;
$labelOn = isset($radioParams['labelOn']) ? $radioParams['labelOn'] : 'right';
$prefix = isset($radioParams['prefix']) ? $radioParams['prefix'] : '';
$suffix = isset($radioParams['suffix']) ? $radioParams['suffix'] : '';
unset($radioParams['expand'], $radioParams['labelOn'], $radioParams['prefix'], $radioParams['suffix']);
unset($radioParams['options'], $radioParams['useEmpty'], $radioParams['emptyText']);
$selected = isset($parameters['value']) ? (string)$parameters['value'] : '';
foreach ($options as $value => $label) {
$radioParams['id'] = $radioId = $id . '_' . $i;
$radioParams['value'] = $value;
if ((string)$value === $selected) {
$radioParams['checked'] = 'checked';
} else {
$radioParams['checked'] = null;
}
$radio = Tag::radioField($radioParams);
if ($labelOn == 'right') {
$radio .= PHP_EOL . $label;
} else {
$radio = $label . PHP_EOL . $radio;
}
$radio = static::label(['for' => $radioId], $radio);
++$i;
$html[] = $prefix . $radio . $suffix;
}
return implode(PHP_EOL, $html);
}
return Tag::select($parameters, $options);
}
protected static function checkRequiredParameters(array $parameters, array $requiredFields)
{
foreach ($requiredFields as $field) {
if (!isset($parameters[$field])) {
throw new WidgetException(__('"%field%" field is required', ['field' => $field]));
}
}
}
public static function ideHelperGenerator()
{
$classContent = [];
foreach (static::$widgets as $name => $definition) {
$parameters = [];
if ($definition instanceof Closure) {
$reflection = new ReflectionFunction($definition);
foreach ($reflection->getParameters() as $param) {
$parameters[] = Parameter::cast($param);
}
$parameters = implode(', ', $parameters);
$classContent[] = " public static function {$name}({$parameters}) {}";
} elseif (is_array($definition)) {
list($class, $method) = $definition;
$reflection = new ReflectionMethod($class, $method);
$invocationParameters = [];
foreach ($reflection->getParameters() as $param) {
$parameters[] = Parameter::cast($param);
$invocationParameters[] = '$' . $param->getName();
}
$parameters = str_replace(["\r", "\n", ' ('], ['', '', '('], implode(', ', $parameters));
$invocationParameters = implode(', ', $invocationParameters);
$isStaticCall = is_string($class);
$invocation = $isStaticCall ? $class . '::' : '(new ' . get_class($class) . ')->';
$invocation .= $method;
$classContent[] = <<<METHOD
public static function {$name}({$parameters}) {
return {$invocation}({$invocationParameters});
}
METHOD;
} else {
$classContent[] = " public static function {$name}() {}";
}
}
return implode(PHP_EOL . PHP_EOL, $classContent);
}
}
<?php
/**
* Created by PhpStorm.
* User: kingrainy
* Date: 2018/1/24
* Time: 上午9:32
*/
namespace Phwoolcon\WebSocket;
use Phwoolcon\Config;
use GatewayWorker\Lib\Gateway;
class Service extends Gateway
{
public static $registerAddress = '127.0.0.1:12808';
}
\ No newline at end of file
<?php
namespace Phwoolcon\Tests\Helper\Admin;
use Phwoolcon\Controller\Admin\ConfigTrait;
class TestConfigTrait
{
use ConfigTrait {
filterConfig as public;
getCurrentConfig as public;
keyList as public;
submitConfig as public;
}
}
<?php
namespace Phwoolcon\Tests\Helper\Cli;
use Phwoolcon\Cli\Command;
use Symfony\Component\Console\Input\InputArgument;
class TestCommand extends Command
{
protected function configure()
{
$this->addArgument('action', InputArgument::OPTIONAL, 'The test action');
}
public function fire()
{
$action = $this->input->getArgument('action');
$this->{'test' . ucfirst($action)}();
}
public function testProgress()
{
$progress = $this->createProgressBar(10);
$progress->start();
$progress->advance();
$progress->advance();
}
public function testQuestion()
{
$this->question('foo');
}
public function testTimestampMessage()
{
$this->outputTimestamp = true;
$this->info('foo');
$this->outputTimestamp = false;
}
}
<?php
namespace Phwoolcon\Tests\Helper;
use Phwoolcon\Cli;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\BufferedOutput;
class CliTestCase extends TestCase
{
/**
* @var \Symfony\Component\Console\Application
*/
protected $cli;
/**
* @var BufferedOutput
*/
protected $output;
public function setUp()
{
parent::setUp();
$_SERVER['PHWOOLCON_MIGRATION_PATH'] = TEST_ROOT_PATH . '/bin/migrations';
$this->cli = Cli::register($this->di);
$this->cli->setAutoExit(false);
$this->output = new BufferedOutput();
}
protected function runCommand($command, $arguments = [])
{
$this->cli->run(new ArgvInput(array_merge([
'cli',
$command,
], $arguments)), $this->output);
return $this->output->fetch();
}
}
<?php
namespace Phwoolcon\Tests\Helper\Filter;
use Phwoolcon\Exception\Http\ForbiddenException;
use Phwoolcon\Router\FilterInterface;
use Phwoolcon\Router\FilterTrait;
class AlwaysException implements FilterInterface
{
use FilterTrait;
protected function filter($uri, $route, $router)
{
throw new ForbiddenException('ALWAYS EXCEPTION', 'foo: bar');
}
}
<?php
namespace Phwoolcon\Tests\Helper\Filter;
use Phwoolcon\Router\FilterInterface;
use Phwoolcon\Router\FilterTrait;
class AlwaysFail implements FilterInterface
{
use FilterTrait;
protected function filter($uri, $route, $router)
{
return false;
}
}
<?php
namespace Phwoolcon\Tests\Helper\Model;
class TestDynamicTrait
{
use NonExistingModelTrait;
}
<?php
namespace Phwoolcon\Tests\Helper;
use Phwoolcon\Controller;
use Phwoolcon\Controller\Admin;
class TestAdminController extends Controller
{
use Admin;
public function getTestRoute()
{
$this->response->setContent('Test Admin Route Content');
}
}
<?php
namespace Phwoolcon\Tests\Helper;
use Phwoolcon\Controller;
use Phwoolcon\Controller\Api;
class TestApiController extends Controller
{
use Api;
public function getTestRoute()
{
$this->response->setContent('Test Api Route Content');
}
public function getJsonApiData()
{
$this->jsonApiReturnData([
'id' => 1,
'type' => 'entity',
'attributes' => [
'foo' => 'bar',
],
]);
}
public function getJsonApiError()
{
$this->jsonApiReturnErrors([
[
'code' => 'foo',
'title' => 'bar',
],
]);
}
public function getJsonApiMeta()
{
$this->jsonApiReturnMeta([
'meta_foo' => 'bar',
]);
}
}
<?php
namespace Phwoolcon\Tests\Helper;
use Phwoolcon\TestStarter\TestCase as TestStarter;
class TestCase extends TestStarter
{
}
<?php
namespace Phwoolcon\Tests\Helper;
use Phwoolcon\Controller;
class TestController extends Controller
{
public function getTestRoute()
{
$this->response->setContent('Test Controllers Route Content');
}
public function getTestPrefixedRoute()
{
$this->response->setContent('Test Prefixed Route Content');
}
public function getTestInput()
{
$this->response->setContent($this->input('key'));
}
public function testJsonReturn($data)
{
$this->jsonReturn($data);
}
public function testInput($key, $default = null)
{
return $this->input($key, $default);
}
public function testRedirect($url)
{
$this->redirect($url);
}
public function testSetBrowserCache($pageId, $type = null)
{
$this->setBrowserCache($pageId, $type);
}
public function testGetBrowserCache($pageId, $type = null)
{
return $this->getBrowserCache($pageId, $type);
}
}
<?php
namespace Phwoolcon\Tests\Helper;
use Phalcon\Db\Column;
use Phalcon\Validation;
use Phalcon\Validation\Validator\StringLength;
use Phwoolcon\Db;
use Phwoolcon\Model;
/**
* Class TestModel
* @package Phwoolcon\Tests
*
* @method string getKey()
* @method mixed getValue()
* @method $this setValue(mixed $value)
*/
class TestModel extends Model
{
protected $_table = 'test_model';
protected $_pk = 'key';
protected $_jsonFields = ['value'];
protected function createTable()
{
$db = Db::connection();
$db->createTable($this->_table, null, [
'columns' => [
new Column('key', [
'type' => Column::TYPE_VARCHAR,
'size' => 32,
'notNull' => true,
'primary' => true,
]),
new Column('value', [
'type' => Column::TYPE_TEXT,
]),
new Column('default_value', [
'type' => Column::TYPE_VARCHAR,
'notNull' => true,
'default' => '',
]),
new Column('created_at', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => false,
]),
new Column('updated_at', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => false,
]),
],
]);
}
public function initialize()
{
parent::initialize();
$db = Db::connection();
$db->tableExists($this->_table) or $this->createTable();
$db->delete($this->_table);
}
public function validation()
{
if ($_SERVER['PHWOOLCON_PHALCON_VERSION'] > 2010000) {
$validator = new Validation();
$validator->add('key', new StringLength([
'min' => 3,
'max' => 32,
]));
} else {
$validator = new \Phalcon\Mvc\Model\Validator\StringLength([
'field' => 'key',
'min' => 3,
'max' => 32,
]);
}
$this->validate($validator);
return parent::validation();
}
}
<?php
namespace Phwoolcon\Tests\Helper;
use Exception;
class TestQueueWorker
{
protected static $jobData;
protected static $memoryLeak = [];
public function diSharedWorker($job, $data)
{
static::$jobData = $data;
}
public static function getJobData()
{
return static::$jobData;
}
public function objectWorker($job, $data)
{
static::$jobData = $data;
}
public static function reset()
{
static::$jobData = null;
static::$memoryLeak = [];
}
public static function staticWorker($job, $data)
{
static::$jobData = $data;
}
public static function staticFailureWorker($job, $data)
{
static::$jobData = $data;
throw new Exception('Failure worker');
}
public static function memoryLeakWorker($job, $data)
{
for ($i = 0; $i < 1000; ++$i) {
static::$memoryLeak[] = $data;
}
}
}
<?php
namespace Phwoolcon\Tests\Helper;
use Exception;
use Phwoolcon\Daemon\Service;
use Phwoolcon\Db;
use Phwoolcon\Log;
use Swoole\Process as SwooleProcess;
use Swoole\Server as SwooleServer;
class TestService extends Service
{
/**
* @var \Phwoolcon\Tests\Unit\Daemon\ServiceTest
*/
protected $testCase;
protected $workerStarted;
public function __construct($config)
{
$this->workerStarted = storagePath('service-worker-started.php');
parent::__construct($config);
}
public function deleteSockFile()
{
$this->choosePort();
is_file($this->sockFile) and unlink($this->sockFile);
}
public function getRunDir()
{
return $this->runDir;
}
public function isWorkerStarted()
{
return include($this->workerStarted);
}
protected function initSwoole()
{
parent::initSwoole();
$server = $this->swoole;
$server->on('WorkerStop', [$this, 'onWorkerStop']);
$server->on('ManagerStop', [$this, 'onManagerStop']);
}
public function onManagerStart(SwooleServer $server)
{
parent::onManagerStart($server);
Log::debug(posix_getpid() . ' onManagerStart ' . $this->swoolePort);
}
public function onManagerStop(SwooleServer $server)
{
Log::debug(posix_getpid() . ' onManagerStop ' . $this->swoolePort);
$this->testCase and $this->testCase->writeRemoteCoverage();
}
public function onShutdown(SwooleServer $server)
{
parent::onShutdown($server);
Log::debug($this->pid . ' onShutdown ' . $this->swoolePort);
$this->testCase and $this->testCase->writeRemoteCoverage();
}
public function onStart(SwooleServer $server)
{
parent::onStart($server);
Log::debug($this->pid . ' onStart ' . $this->swoolePort);
}
public function onWorkerStart(SwooleServer $server, $workerId)
{
parent::onWorkerStart($server, $workerId);
$this->setWorkerStarted();
Log::debug(posix_getpid() . ' onWorkerStart ' . $this->swoolePort);
}
public function onWorkerStop(SwooleServer $server, $workerId)
{
Log::debug(posix_getpid() . ' onWorkerStop ' . $this->swoolePort);
$this->testCase and $this->testCase->writeRemoteCoverage();
}
/**
* @param \Phwoolcon\Tests\Unit\Daemon\ServiceTest $testCase
* @return $this
*/
public function setTestCase($testCase)
{
$this->testCase = $testCase;
return $this;
}
public function setWorkerStarted($flag = true)
{
fileSaveArray($this->workerStarted, $flag);
}
public function start($dryRun = false)
{
try {
$dryRun or Log::debug('Starting...');
return parent::start($dryRun);
} catch (Exception $e) {
Log::exception($e);
}
return false;
}
public function startIsolated()
{
$this->setWorkerStarted(false);
$serverProcess = new SwooleProcess(function () {
$this->start();
}, true);
$serverProcess->start();
Db::reconnect();
$retry = 20;
while ($retry && !$this->isWorkerStarted()) {
usleep(1e5);
--$retry;
}
$this->isWorkerStarted() or Log::error('Failed to start');
return $serverProcess;
}
public function stop($instance = 'current')
{
parent::stop($instance);
if ($serviceInfo = $this->getServiceInfo($instance)) {
list(, , $port) = array_values($serviceInfo);
$this->showStatus($port, false, $error);
$retry = 10;
while (!$error) {
usleep(1e5);
$this->showStatus($port, false, $error);
--$retry;
}
$error or Log::error('Unable to stop instance: ' . $instance);
}
}
}
<?php
namespace Phwoolcon\Tests\Helper;
use Phalcon\Db\Column;
use Phalcon\Db\Index;
use Phwoolcon\Db;
use Phwoolcon\Model\User;
class TestUserModel extends User
{
protected function createTable()
{
$db = Db::connection();
$db->createTable($this->_table, null, [
'columns' => [
new Column('id', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => true,
'primary' => true,
]),
new Column('username', [
'type' => Column::TYPE_VARCHAR,
'size' => 20,
'notNull' => false,
]),
new Column('email', [
'type' => Column::TYPE_VARCHAR,
'size' => 255,
'notNull' => false,
]),
new Column('mobile', [
'type' => Column::TYPE_VARCHAR,
'size' => 20,
'notNull' => false,
]),
new Column('password', [
'type' => Column::TYPE_VARCHAR,
'size' => 160,
'notNull' => true,
]),
new Column('confirmed', [
'type' => Column::TYPE_BOOLEAN,
'size' => 1,
'unsigned' => true,
'notNull' => true,
'default' => 0,
]),
new Column('created_at', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => false,
]),
new Column('updated_at', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => false,
]),
],
'indexes' => [
new Index('username', ['username'], 'UNIQUE'),
new Index('email', ['email'], 'UNIQUE'),
new Index('mobile', ['mobile'], 'UNIQUE'),
],
'options' => [
'TABLE_COLLATION' => Db::getDefaultTableCharset(),
],
]);
}
public function initialize()
{
parent::initialize();
$db = Db::connection();
$db->tableExists($this->_table) or $this->createTable();
$db->delete($this->_table);
}
}
<?php
namespace Phwoolcon\Tests\Helper;
use Phalcon\Db\Column;
use Phalcon\Db\Index;
use Phalcon\Db\Reference;
use Phwoolcon\Db;
use Phwoolcon\Model\UserProfile;
class TestUserProfileModel extends UserProfile
{
protected function createTable()
{
$db = Db::connection();
$db->createTable($this->_table, null, [
'columns' => [
new Column('user_id', [
'type' => Column::TYPE_BIGINTEGER,
'size' => 20,
'unsigned' => true,
'notNull' => true,
'primary' => true,
]),
new Column('real_name', [
'type' => Column::TYPE_VARCHAR,
'size' => 32,
'notNull' => false,
]),
new Column('avatar', [
'type' => Column::TYPE_VARCHAR,
'size' => 255,
'notNull' => true,
'default' => '',
]),
new Column('remember_token', [
'type' => Column::TYPE_VARCHAR,
'size' => 52,
'notNull' => false,
]),
new Column('extra_data', [
'type' => Column::TYPE_TEXT,
'notNull' => false,
]),
],
'references' => [
new Reference('user_profile_users_user_id', [
'referencedTable' => 'users',
'columns' => ['user_id'],
'referencedColumns' => ['id'],
'onDelete' => 'CASCADE',
'onUpdate' => 'CASCADE',
]),
],
'options' => [
'TABLE_COLLATION' => Db::getDefaultTableCharset(),
],
]);
}
public function initialize()
{
parent::initialize();
$db = Db::connection();
$db->tableExists($this->_table) or $this->createTable();
$db->delete($this->_table);
}
}
<?php
namespace Phwoolcon\Tests\Helper;
use Phwoolcon\View\Widget;
class TestWidget extends Widget
{
public function hello($p1)
{
return "Hello {$p1}";
}
public static function helloStatic($p1, array $p2 = [])
{
return "Hello {$p1}, " . var_export($p2);
}
}
<?php
namespace Phwoolcon\Tests\Integration\Cli;
use Phwoolcon\Cli\Command\Migrate;
use Phwoolcon\Tests\Helper\CliTestCase;
use Symfony\Component\Console\Input\ArgvInput;
class MigrationTest extends CliTestCase
{
/**
* @var Migrate
*/
protected $migrateCommand;
public function setUp()
{
parent::setUp();
$this->migrateCommand = $this->cli->get('migrate');
$this->migrateCommand->cleanMigrationsTable();
}
protected function createMigrateFile()
{
foreach (glob(migrationPath('/*test.php')) as $file) {
@unlink($file);
}
$output = $this->runCommand('migrate:create', ['test']);
preg_match('/Created\ Migration:\ (.+)/', $output, $matches);
return fnGet($matches, 1);
}
public function testMigrateCreate()
{
$migrationFile = $this->createMigrateFile();
/* @var \Phwoolcon\Cli\Command\MigrateCreate $command */
$command = $this->cli->get('migrate:create');
$this->assertNotEmpty($migrationFile, 'Unable to create migration file');
$this->assertFileExists($savedFile = migrationPath($migrationFile), 'Unable to write migration file');
$this->assertEquals(
$command->template(),
file_get_contents($savedFile),
'Bad content written to migration file'
);
}
public function testMigrateUpAndDown()
{
$migrationFile = $this->createMigrateFile();
/* @var \Phwoolcon\Cli\Command\MigrateList $list */
$list = $this->cli->get('migrate:list');
// Test migration list
$output = $this->runCommand('migrate:list');
$this->assertContains($migrationFile, $output, 'Unable to detect migrations');
$list->clearMigratedCache();
// Test migration up
$output = $this->runCommand('migrate:up');
$this->assertContains(
sprintf('Start migration "%s"... [ OK ]', $migrationFile),
$output,
'Unable to run migration'
);
$output = $this->runCommand('migrate:list', ['-i']);
$this->assertContains($migrationFile, $output, 'Unable to detect installed migrations');
$list->clearMigratedCache();
// Test repeat migration up
$output = $this->runCommand('migrate:up');
$this->assertContains('Nothing to be migrated', $output, 'Should not repeatedly run migrations');
$output = $this->runCommand('migrate:list');
$this->assertContains('Nothing to be migrated', $output, 'Should report no migrations');
$list->clearMigratedCache();
// Test migration down
$output = $this->runCommand('migrate:down', ['-f']);
$this->assertContains(
sprintf('Start reverting migration "%s"... [ OK ]', $migrationFile),
$output,
'Unable to revert migration'
);
$output = $this->runCommand('migrate:list');
$this->assertContains($migrationFile, $output, 'Unable to re-detect migrations');
$list->clearMigratedCache();
// Test migration list -a
$output = $this->runCommand('migrate:list', ['-a']);
$this->assertContains($migrationFile, $output, 'Unable to detect all migrations');
}
}
<?php
namespace Phwoolcon\Tests\Integration\Cli;
use Exception;
use Phwoolcon\Config;
use Phwoolcon\Queue;
use Phwoolcon\Tests\Helper\CliTestCase;
use Phwoolcon\Tests\Helper\TestQueueWorker;
class QueueConsumeTest extends CliTestCase
{
public function setUp()
{
parent::setUp();
Config::set('queue.queues.default_queue.connection', 'beanstalkd');
Config::set('queue.connections.beanstalkd.connect_timeout', 1);
Queue::register($this->di);
$this->clearQueue();
}
protected function clearQueue()
{
/* @var Queue\Adapter\Beanstalkd $connection */
$connection = Queue::connection();
$queue = $connection->getQueue(null);
try {
$pheanstalk = $connection->getConnection();
while ($job = $pheanstalk->peekReady($queue)) {
$pheanstalk->delete($job);
}
} catch (Exception $e) {
}
}
public function testProduceAndConsume()
{
$data = ['foo' => 'bar'];
$connection = Queue::connection();
$connection->push([TestQueueWorker::class, 'staticWorker'], $data);
TestQueueWorker::reset();
$output = $this->runCommand('queue:consume', ['--ttl=0']);
$this->assertEquals($data, TestQueueWorker::getJobData());
$this->assertEmpty($output);
}
public function testMemoryLeak()
{
$memoryLimit = (int)(memory_get_usage() / 1024 / 1024) + 10;
$data = ['foo' => 'bar'];
$connection = Queue::connection();
for ($i = 0; $i < 300; ++$i) {
$connection->push([TestQueueWorker::class, 'memoryLeakWorker'], $data);
}
$output = $this->runCommand('queue:consume', ['--ttl=5', "--memory={$memoryLimit}"]);
$this->assertStringEndsWith('Memory leak protection', trim($output));
TestQueueWorker::reset();
$this->clearQueue();
}
public function testException()
{
$data = ['foo' => 'bar'];
$connection = Queue::connection();
$connection->push([TestQueueWorker::class, 'staticFailureWorker'], $data);
TestQueueWorker::reset();
$output = $this->runCommand('queue:consume', ['--ttl=0']);
$this->assertEquals($data, TestQueueWorker::getJobData());
$this->assertStringEndsWith('Failure worker', trim($output));
TestQueueWorker::reset();
$this->clearQueue();
}
}
<?php
namespace Phwoolcon\Tests\Integration;
use Phwoolcon\Config;
use Phwoolcon\Db;
use Phwoolcon\Tests\Helper\TestCase;
class DbTest extends TestCase
{
public function setUp()
{
parent::setUp();
Db::connection()->dropTable('config');
$this->reloadConfig();
}
public function testGetDefaultTableCharset()
{
$this->assertEquals('utf8_unicode_ci', Db::getDefaultTableCharset());
}
public function testConnection()
{
$this->assertEquals('value', Db::connection()->query('SELECT "value" v')->fetch()['v'], 'Unable to connect DB');
}
public function testReconnect()
{
$this->assertEquals('value', Db::reconnect()->query('SELECT "value" v')->fetch()['v'], 'DB reconnect failed');
}
}
<?php
namespace Phwoolcon\Tests\Integration\Http;
use Phwoolcon\Exception\HttpClientException;
use Phwoolcon\Http\Client;
use Phwoolcon\Tests\Helper\TestCase;
class ClientTest extends TestCase
{
/**
* @var Client
*/
protected $client;
public function setUp()
{
parent::setUp();
Client::register($this->di);
$this->client = Client::getInstance();
}
public function testGet()
{
$client = $this->client;
$url = 'https://api.github.com/users/Fishdrowned/orgs';
$response = Client::get($url);
$this->assertNotEmpty($response);
$this->assertNotEmpty($headers = $client->getLastResponseHeaders());
$this->assertArrayHasKey('content-type', $headers);
$this->assertEquals(200, $client->getLastResponseCode());
$this->assertEquals($url, $client->getLastRequest()['url']);
$this->assertEquals($response, $client->getLastResponse());
$url = 'https://api.github.com/repos/phwoolcon/phwoolcon/issues';
$response = Client::get($url, ['state' => 'closed']);
$this->assertNotEmpty($response);
$this->assertNotEmpty($headers = $client->getLastResponseHeaders());
$this->assertArrayHasKey('content-type', $headers);
$this->assertEquals(200, $client->getLastResponseCode());
$this->assertEquals($url, $client->getLastRequest()['url']);
$this->assertEquals($response, $client->getLastResponse());
}
public function testHead()
{
$client = $this->client;
$url = 'https://api.github.com/users/Fishdrowned/orgs';
$response = Client::head($url);
$this->assertEmpty($response);
$this->assertNotEmpty($headers = $client->getLastResponseHeaders());
$this->assertArrayHasKey('content-type', $headers);
$this->assertEquals(200, $client->getLastResponseCode());
$this->assertEquals($url, $client->getLastRequest()['url']);
$this->assertEquals($response, $client->getLastResponse());
}
public function testPost()
{
$client = $this->client;
$url = 'https://api.github.com/authorizations';
$response = Client::post($url, '{"scopes":["public_repo"]}');
$this->assertNotEmpty($response);
$this->assertNotEmpty($headers = $client->getLastResponseHeaders());
$this->assertArrayHasKey('content-type', $headers);
$this->assertEquals(401, $client->getLastResponseCode());
$this->assertEquals($url, $client->getLastRequest()['url']);
$this->assertEquals($response, $client->getLastResponse());
}
public function testCustomMethod()
{
$client = $this->client;
$url = 'https://api.github.com/some-thing-that-not-exists';
$response = Client::request($url, '', 'DELETE');
$this->assertNotEmpty($response);
$this->assertNotEmpty($headers = $client->getLastResponseHeaders());
$this->assertArrayHasKey('content-type', $headers);
$this->assertEquals(404, $client->getLastResponseCode());
$this->assertEquals($url, $client->getLastRequest()['url']);
$this->assertEquals($response, $client->getLastResponse());
}
public function testErrorAndCustomDns()
{
$client = $this->client;
$options = $client->getOptions();
$invalidIp = '192.0.2.1';
$customDns = "api.no-this-site.dev:80:{$invalidIp}";
ob_start();
$stdOut = fopen('php://output', 'w');
$client->setOptions(array_replace($options, [
'custom_dns' => [$customDns],
'curl_options' => [
CURLOPT_CONNECTTIMEOUT_MS => 100,
CURLOPT_VERBOSE => true,
CURLOPT_STDERR => $stdOut,
],
]));
$url = 'http://api.no-this-site.dev/';
$response = null;
$e = null;
try {
$response = Client::get($url);
} catch (\Exception $e) {
}
fclose($stdOut);
$verboseInfo = ob_get_clean();
$client->setOptions($options);
$this->assertInstanceOf(HttpClientException::class, $e);
$this->assertContains("Added {$customDns} to DNS cache", $verboseInfo);
$this->assertContains("Trying {$invalidIp}...", $verboseInfo);
$this->assertNull($response);
$this->assertNull($client->getLastResponse());
$this->assertEmpty($client->getLastResponseHeaders());
$this->assertEquals($url, $client->getLastCurlInfo()['url']);
$this->assertEquals(0, $client->getLastResponseCode());
$this->assertEquals($url, $client->getLastRequest()['url']);
}
}
<?php
namespace Phwoolcon\Tests\Integration;
use Phwoolcon\Config;
use Phwoolcon\Mailer;
use Phwoolcon\Queue;
use Phwoolcon\Tests\Helper\TestCase;
class MailerTest extends TestCase
{
protected static $smtpPortReady;
public function setUp()
{
parent::setUp();
Queue::register($this->di);
}
public function tearDown()
{
// Enable mailer
Config::set('mail.enabled', true);
// Set async mode off
Config::set('mail.async', false);
Mailer::register($this->di);
parent::tearDown();
}
protected function skipIfSmtpPortNotReady()
{
$host = Config::get('mail.smtp_host');
$port = Config::get('mail.smtp_port');
if (static::$smtpPortReady === null) {
if ($fp = @stream_socket_client("tcp://{$host}:{$port}", $errNo, $errStr, 1)) {
static::$smtpPortReady = true;
fclose($fp);
} else {
static::$smtpPortReady = false;
}
}
if (!static::$smtpPortReady) {
$skipMessage = <<<EOT
SMTP server not ready on {$host}:{$port}, you may need to instal postfix, or use a real SMTP server.
I use smtp-sink (a postfix component) to run a SMTP test server:
smtp-sink -d "%d.%H.%M.%S" localhost:2500 1000
EOT;
$this->markTestSkipped($skipMessage);
}
}
public function testDisableMailer()
{
// Disable mailer
Config::set('mail.enabled', false);
Mailer::register($this->di);
$to = ['john@domain.com' => 'John Doe'];
$subject = 'Hello World';
$body = 'Foo bar';
$result = Mailer::send($to, $subject, $body);
$this->assertEquals(0, $result);
}
public function testSendingEmailAsynchronously()
{
$this->skipIfSmtpPortNotReady();
// Clear db queue
/* @var \Phwoolcon\Queue\Adapter\DbQueue\Connection $dbQueue */
$dbQueue = Queue::connection('async_email_sending')->getConnection();
$dbQueue->getWriteConnection()->delete($dbQueue->getSource());
$this->assertEquals(0, $dbQueue::countSimple(['status' => $dbQueue::STATUS_READY]));
// Prepare queue listener
$listener = new Queue\Listener();
// Set async mode on
Config::set('mail.async', true);
Mailer::register($this->di);
$to = ['to@domain.com' => 'John Doe'];
$subject = 'Hello World';
$body = 'Foo bar';
// Send a email, asynchronously
$result = Mailer::send($to, $subject, $body);
$this->assertEquals(1, $result);
// The email should be in the queue
$this->assertEquals(1, $dbQueue::countSimple(['status' => $dbQueue::STATUS_READY]));
// Pop the queue to send the mail
$popResult = $listener->pop('async_email_sending', null, 0, 0, 1);
$this->assertEquals($popResult::STATUS_SUCCESS, $popResult->getStatus());
// Verify the job payload
$job = $popResult->getJob();
$this->assertInstanceOf(Queue\Adapter\JobInterface::class, $job);
list($worker, $data) = array_values(json_decode($job->getRawBody(), true));
$this->assertEquals([Mailer::class, 'handleQueueJob'], $worker);
list($popTo, $popSubject, $popBody) = $data;
$this->assertEquals($to, $popTo);
$this->assertEquals($subject, $popSubject);
$this->assertEquals($body, $popBody);
// Now the queue should be empty
$this->assertEquals(0, $dbQueue::countSimple(['status' => $dbQueue::STATUS_READY]));
}
public function testSendingEmailSynchronously()
{
$this->skipIfSmtpPortNotReady();
// Set async mode off
Config::set('mail.async', false);
Mailer::register($this->di);
$to = ['john@domain.com' => 'John Doe'];
$subject = 'Hello World';
$body = 'Foo bar';
// Send a email, synchronously
$result = Mailer::send($to, $subject, $body);
$this->assertEquals(1, $result);
// Send to multiple users, synchronously
$to = ['john@domain.com' => 'John Doe', 'jane@domain.com' => 'Jane Doe'];
$result = Mailer::send($to, $subject, $body, Mailer::CONTENT_TYPE_TEXT);
$this->assertEquals(2, $result);
}
public function testAttachmentsAndEmbeds()
{
$this->skipIfSmtpPortNotReady();
// Set async mode off
Config::set('mail.async', false);
Mailer::register($this->di);
$to = ['john@domain.com' => 'John Doe'];
$subject = 'Hello World';
// Send email with attachment in data
$body = [
'body' => 'Foo bar',
'attach' => [
'data' => 'Foo text',
'file_name' => 'foo.txt',
'file_type' => 'text/plain',
],
];
$result = Mailer::send($to, $subject, $body);
$this->assertEquals(1, $result);
// Send email with attachment in file path
$body = [
'body' => 'Foo bar',
'attach' => [
'path' => __FILE__,
],
];
$result = Mailer::send($to, $subject, $body);
$this->assertEquals(1, $result);
// Send email with attachment in file path
$body = [
'body' => 'Foo bar',
'attach' => [
'path' => __FILE__,
],
];
$result = Mailer::send($to, $subject, $body);
$this->assertEquals(1, $result);
// Send email with embed media
$body = [
'body' => 'Foo bar <img src="%img1%">',
'embed' => [
'img1' => __FILE__,
],
];
$result = Mailer::send($to, $subject, $body);
$this->assertEquals(1, $result);
}
}
<?php
namespace Phwoolcon\Tests\Integration\Model;
use Phwoolcon\Config;
use Phwoolcon\Model\Config as ConfigModel;
use Phwoolcon\Tests\Helper\TestCase;
class ConfigTest extends TestCase
{
public function testSaveConfig()
{
// Test save array value
$key = 'test_save';
$subKey = $key . '.foo';
$value = ['foo' => $subValue = 'bar'];
ConfigModel::saveConfig($key, $value);
$this->reloadConfig();
$this->assertEquals($value, Config::get($key));
// Test save null value
ConfigModel::saveConfig($key, null);
$this->assertFalse(ConfigModel::findFirstSimple(['key' => $key]));
// Test save a non-existing sub key
ConfigModel::saveConfig($subKey, $subValue);
$this->reloadConfig();
$this->assertEquals($subValue, Config::get($subKey));
// Test save a existing sub key
$subValue = 'baz';
ConfigModel::saveConfig($subKey, $subValue);
$this->reloadConfig();
$this->assertEquals($subValue, Config::get($subKey));
}
}
<?php
namespace Phwoolcon\Tests\Integration;
use Exception;
use Phalcon\Mvc\Model\ValidationFailed;
use Phwoolcon\Cache\Clearer;
use Phwoolcon\Db;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\Tests\Helper\TestModel;
class ModelTest extends TestCase
{
protected function getModelInstance()
{
return new TestModel;
}
public function setUp()
{
parent::setUp();
Clearer::clear(Clearer::TYPE_META);
}
public function testSetDataAndGetData()
{
$model = $this->getModelInstance();
$key = 'test';
$value = 'test value';
$model->setData($key, $value);
$this->assertEquals($value, $model->getData($key));
$this->assertEquals($value, $model->getAdditionalData($key));
}
public function testAddData()
{
$model = $this->getModelInstance();
$model->addData($data = [
'key' => 'k',
'value' => 'v',
'non-existing' => 'n',
]);
$storedData = $data;
unset($storedData['non-existing']);
$storedData['default_value'] = $model->default_value;
$storedData['created_at'] = $model->created_at;
$storedData['updated_at'] = $model->updated_at;
$this->assertEquals($storedData, $model->getData());
}
public function testSave()
{
$model = $this->getModelInstance();
$model->setValue($value = ['test__call' => 'Test __call() setValue']);
$this->assertEmpty($model->getStringMessages());
$this->assertEquals($value, $model->getValue());
$this->assertTrue($model->save(), 'Unable to save model');
$this->assertNotEmpty($key = $model->getKey(), 'Unable to generate id');
$model->setValue(['hello' => 'world']);
$this->assertTrue($model->save(), 'Unable to save model continuously');
$this->assertEquals($key, $model->getKey());
}
public function testLoad()
{
$model = $this->getModelInstance();
$model->setData($value = [
'key' => $key = 'test-key',
'value' => ['foo' => 'bar'],
]);
$this->assertTrue($model->save(), $model->getStringMessages());
$found = $model::findFirstSimple(compact('key'));
$this->assertInstanceOf(get_class($model), $found, 'Unable to load model from db');
$value['default_value'] = $model->default_value;
$value['created_at'] = $model->created_at;
$value['updated_at'] = $model->updated_at;
$this->assertEquals($value, $found->getData(), 'Bad db loaded value');
}
public function testLoadList()
{
$model = $this->getModelInstance();
$model->setData($value = [
'key' => $key = 'test-key2',
'value' => ['foo' => 'bar'],
]);
$this->assertTrue($model->save(), $model->getStringMessages());
// Test findSimple
$list = $model::findSimple(['key' => ['LIKE', 'test%']], [], 'key ASC', '*', 10);
$this->assertGreaterThan(0, $list->count(), 'Unable to load model list from db');
// Test findSimple, limit with offset
$list = $model::findSimple(['key' => ['LIKE', 'test%']], [], 'key ASC', '*', '10, 0');
$this->assertGreaterThan(0, $list->count(), 'Unable to load model list from db');
// Test countSimple
$this->assertGreaterThan(
0,
$model::countSimple('key LIKE :test_bind:', ['test_bind' => 'test%']),
'Unable to get model count from db'
);
$this->assertGreaterThan(0, $model::countSimple('key LIKE "test%"'), 'Unable to get model count from db');
}
public function testMutablePrimaryKey()
{
$model = $this->getModelInstance();
$model->setData($value = [
'key' => $key = 'test-mutable-key',
'value' => ['foo' => 'bar'],
]);
$this->assertTrue($model->save(), $model->getStringMessages());
$this->assertInstanceOf(get_class($model), $found = $model::findFirstSimple(compact('key')));
$found->key = $key = 'test-mutated-key';
$this->assertTrue($found->save(), $found->getStringMessages());
$this->assertInstanceOf(get_class($model), $found = $model::findFirstSimple(compact('key')));
}
public function testExecuteSql()
{
$model = $this->getModelInstance();
$model->setData([
'key' => $key = 'test-key3',
'value' => ['foo' => 'bar'],
]);
$this->assertTrue($model->save(), $model->getStringMessages());
$table = $model->getSource();
// Test sqlFetchAll
$this->assertNotEmpty($model->sqlFetchAll("SELECT * FROM {$table} WHERE `key` LIKE :test_bind:", [
'test_bind' => 'test%',
]));
$this->assertNotEmpty($model->sqlFetchAll("SELECT * FROM {$table} WHERE `key` IN ({test_bind:array})", [
'test_bind' => [$key],
]));
// Test sqlFetchColumn
$this->assertEquals(2, (int)$model->sqlFetchColumn("SELECT 1 + 1"));
// Test sqlExecute INSERT
$key = 'test-key4';
$value = 'foo';
$selectSql = "SELECT `value` FROM {$table} WHERE `key` IN ({test_bind:array})";
$this->assertTrue($model->sqlExecute("INSERT INTO {$table} (`key`, `value`) VALUES ('{$key}', '{$value}')"));
$this->assertEquals($value, $model->sqlFetchColumn($selectSql, [
'test_bind' => [$key],
]));
// Test sqlExecute UPDATE
$value = 'bar';
$updateSql = "UPDATE {$table} SET `value` = '{$value}' WHERE `key` IN ({test_bind:array})";
$this->assertTrue($model->sqlExecute($updateSql, [
'test_bind' => [$key],
]));
$this->assertEquals($value, $model->sqlFetchColumn($selectSql, [
'test_bind' => [$key],
]));
// Test sqlExecute DELETE
$this->assertTrue($model->sqlExecute("DELETE FROM {$table} WHERE `key` IN ({test_bind:array})", [
'test_bind' => [$key],
]));
$this->assertFalse($model->sqlFetchColumn($selectSql, [
'test_bind' => [$key],
]));
}
public function testReset()
{
$model = $this->getModelInstance();
$defaultData = $model->getData();
$model->setData([
'key' => $key = 'test-key',
'value' => ['foo' => 'bar'],
]);
$this->assertNotEquals($defaultData, $model->getData(), 'Unable to set data');
$this->assertTrue($model->save(), $model->getStringMessages());
$this->assertFalse($model->isNew(), 'Unable to set isNew after save');
$model->reset();
$this->assertEquals($defaultData, $model->getData(), 'Unable to reset');
$this->assertTrue($model->isNew(), 'Unable to reset isNew status');
}
public function testErrorMessage()
{
$model = $this->getModelInstance();
$model->setId('1');
$e = false;
try {
$model->save();
} catch (Exception $e) {
}
$this->assertInstanceOf(ValidationFailed::class, $e);
$this->assertNotEmpty($model->getStringMessages(), 'Model pre save validation not triggered');
}
}
<?php
namespace Phwoolcon\Tests\Integration;
use Exception;
use Phwoolcon\Config;
use Phwoolcon\Queue;
use Phwoolcon\Queue\Listener;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\Tests\Helper\TestQueueWorker;
class QueueTest extends TestCase
{
public function setUp()
{
parent::setUp();
Queue::register($this->di);
}
protected function realTestQueuePushAndFire($driver)
{
Config::set('queue.queues.default_queue.connection', $driver);
Queue::register($this->di);
$queue = Queue::connection();
$workerClass = TestQueueWorker::class;
$data = [
'foo' => 'bar',
];
TestQueueWorker::reset();
$queue->push($jobName = "{$workerClass}::staticWorker", $data);
$job = $queue->pop();
$this->assertEquals($jobName, $job->getName());
$this->assertNull(TestQueueWorker::getJobData());
$job->fire();
$this->assertEquals($data, TestQueueWorker::getJobData(), 'Unable to fire static queue worker');
TestQueueWorker::reset();
$queue->push("{$workerClass}->objectWorker", $data);
$job = $queue->pop();
$this->assertNull(TestQueueWorker::getJobData());
$job->fire();
$this->assertEquals($data, TestQueueWorker::getJobData(), 'Unable to fire object queue worker');
TestQueueWorker::reset();
$queue->push("{$workerClass}@diSharedWorker", $data);
$job = $queue->pop();
$this->assertNull(TestQueueWorker::getJobData());
$job->fire();
$this->assertEquals($data, TestQueueWorker::getJobData(), 'Unable to fire di shared queue worker');
}
public function testBeanstalkdPushAndFire()
{
$this->realTestQueuePushAndFire('beanstalkd');
}
public function testDbQueuePushAndFire()
{
$this->realTestQueuePushAndFire('db');
}
public function testBeanstalkdQueueJobsOperation()
{
$this->realTestQueueJobsOperation('beanstalkd');
}
public function testDbQueueJobsOperation()
{
$this->realTestQueueJobsOperation('db');
}
public function realTestQueueJobsOperation($driver)
{
Config::set('queue.queues.default_queue.connection', $driver);
Queue::register($this->di);
/* @var Queue\Adapter\Beanstalkd $queue */
$queue = Queue::connection();
$data = 'Test raw data';
// Push
$jobId = $queue->pushRaw($data);
$this->assertTrue(is_numeric($jobId));
$attempts = 0;
// Pop
/* @var Queue\Adapter\Beanstalkd\Job $job */
$job = $queue->pop();
++$attempts;
$this->assertEquals($data, $job->getRawBody());
$this->assertEquals($jobId, $job->getJobId());
// Release
$job->release();
// Bury
$job = $queue->pop();
++$attempts;
$this->assertEquals($data, $job->getRawBody());
$this->assertEquals($jobId, $job->getJobId());
$job->bury();
// Pop empty
$job = $queue->pop();
$this->assertNull($job);
// Kick
$this->assertTrue(is_numeric($queue->getConnection()->kick(1)));
// Attempts
$job = $queue->pop();
++$attempts;
$this->assertEquals($jobId, $job->getJobId());
$this->assertEquals($attempts, $job->attempts());
// Delete
$this->assertFalse($job->isDeleted());
$job->delete();
$this->assertTrue($job->isDeleted());
}
public function testListener()
{
Queue::register($this->di);
$queue = Queue::connection();
$data = [
'test' => 'listener',
];
TestQueueWorker::reset();
$queue->push([TestQueueWorker::class, 'staticWorker'], $data);
// Listen and fire job
$listener = new Listener();
$listener->pop('', null, 0, 0, 1);
$this->assertEquals($data, TestQueueWorker::getJobData(), 'Unable to fire queue listener');
// Pop nothing from a empty queue
$result = $listener->pop('', null, 0, 0, 1);
$this->assertNull($result->getJob());
$this->assertEquals($result::STATUS_SUCCESS, $result->getStatus());
}
public function testFailedListener()
{
Queue::register($this->di);
$queue = Queue::connection();
$workerClass = TestQueueWorker::class;
$data = [
'test' => 'listener',
];
TestQueueWorker::reset();
$queue->push("{$workerClass}::staticFailureWorker", $data);
// Failure
$listener = new Listener();
$e = null;
try {
$listener->pop('', null, 0, 0, 1);
} catch (Exception $e) {
}
$this->assertInstanceOf(Exception::class, $e);
// Log failure job
$result = $listener->pop('', null, 0, 0, 1);
$this->assertEquals($result::STATUS_FAILED, $result->getStatus());
}
}
<?php
namespace Phwoolcon\Tests\Integration;
use Phwoolcon\Db;
use Phwoolcon\Model\User;
use Phwoolcon\Model\UserProfile;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\Tests\Helper\TestUserModel;
use Phwoolcon\Tests\Helper\TestUserProfileModel;
class UserModelTest extends TestCase
{
/**
* @return TestUserModel
*/
protected function getUserModelInstance()
{
return $this->di->get(User::class);
}
/**
* @return TestUserModel
*/
protected function getTestUser()
{
$user = $this->getUserModelInstance();
if ($tmp = $user::findFirstSimple(['username' => 'Test'])) {
$user = $tmp;
} else {
$user->setData([
'username' => 'Test',
'password' => '123456',
'confirmed' => '1',
]);
$user->save();
}
return $user;
}
public function setUp()
{
parent::setUp();
Db::clearMetadata();
$this->di->set(User::class, TestUserModel::class);
$this->di->set(UserProfile::class, TestUserProfileModel::class);
$this->getUserModelInstance()->delete();
}
public function testCreateUser()
{
$user = $this->getUserModelInstance();
$user->setData([
'username' => 'Test',
'password' => '123456',
'confirmed' => '1',
]);
$this->assertTrue($user->save());
}
public function testGetUserFields()
{
$user = $this->getTestUser();
$this->assertEquals('Test', $user->getUsername());
$this->assertNull($user->getEmail());
$this->assertNull($user->getMobile());
}
public function testRememberToken()
{
$user = $this->getTestUser();
$user->setRememberToken($token = '12345678');
$this->assertTrue($user->save());
$this->assertEquals($token, $user->getRememberToken());
$user->removeRememberToken();
$this->assertTrue($user->save());
$this->assertEmpty($user->getRememberToken());
}
public function testProfileExtraData()
{
$user = $this->getTestUser();
$profile = $user->getUserProfile();
$this->assertEquals(TestUserProfileModel::class, get_class($profile));
$profile->setExtraData($key = 'test_extra_data', $value = ['foo' => 'bar']);
$this->assertTrue($profile->save());
$this->assertEquals($value, $profile->getExtraData($key));
}
public function testGetAvatar()
{
$user = $this->getTestUser();
$profile = $user->getUserProfile();
$avatarUrl = $user->getAvatar();
$avatarPath = $profile->getAvatar();
$this->assertNotEmpty($avatarPath);
$this->assertEquals(url($avatarPath), $avatarUrl);
}
public function testResetPasswordToken()
{
$user = $this->getTestUser();
$profile = $user->getUserProfile();
$token = $profile->generateResetPasswordToken();
$this->assertNotEmpty($token);
$this->assertEquals($token, $profile->getResetPasswordToken());
$this->assertNotEmpty($profile->getResetPasswordTokenCreatedAt());
$profile->removeResetPasswordToken();
$this->assertEmpty($profile->getResetPasswordToken());
$this->assertEmpty($profile->getResetPasswordTokenCreatedAt());
}
}
<?php
namespace Phwoolcon\Tests\Integration\Util;
use Phwoolcon\Config;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\Util\Counter;
class AutoCounterTest extends TestCase
{
public function setUp()
{
parent::setUp();
Config::set('counter.default', 'auto');
Counter::register($this->di);
}
public function tearDown()
{
Config::set('counter.default', 'auto');
parent::tearDown();
}
public function testIncrement()
{
$key1 = 'test';
$key2 = 'test2';
Counter::reset($key1);
Counter::reset($key2);
$this->assertEquals(1, Counter::increment($key1));
$this->assertEquals(2, Counter::increment($key1));
$this->assertEquals(1, Counter::increment($key2));
$this->assertEquals(2, Counter::increment($key2));
$this->assertEquals(4, Counter::increment($key2, 2));
$this->assertEquals(4, Counter::increment($key1, 2));
}
public function testDecrement()
{
$key1 = 'test';
$key2 = 'test2';
Counter::reset($key1);
Counter::reset($key2);
$this->assertEquals(10, Counter::increment($key1, 10));
$this->assertEquals(10, Counter::increment($key2, 10));
$this->assertEquals(9, Counter::decrement($key1));
$this->assertEquals(8, Counter::decrement($key1));
$this->assertEquals(9, Counter::decrement($key2));
$this->assertEquals(8, Counter::decrement($key2));
$this->assertEquals(6, Counter::decrement($key2, 2));
$this->assertEquals(6, Counter::decrement($key1, 2));
$this->assertEquals(-2, Counter::decrement($key2, 8));
$this->assertEquals(-2, Counter::decrement($key1, 8));
}
}
<?php
namespace Phwoolcon\Tests\Integration\Util;
use Phwoolcon\Config;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\Util\Counter;
class CacheCounterTest extends TestCase
{
public function setUp()
{
parent::setUp();
Config::set('counter.default', 'cache');
Counter::register($this->di);
}
public function tearDown()
{
Config::set('counter.default', 'auto');
parent::tearDown();
}
public function testIncrement()
{
$key1 = 'test';
$key2 = 'test2';
Counter::reset($key1);
Counter::reset($key2);
$this->assertEquals(1, Counter::increment($key1));
$this->assertEquals(2, Counter::increment($key1));
$this->assertEquals(1, Counter::increment($key2));
$this->assertEquals(2, Counter::increment($key2));
$this->assertEquals(4, Counter::increment($key2, 2));
$this->assertEquals(4, Counter::increment($key1, 2));
}
public function testDecrement()
{
$key1 = 'test';
$key2 = 'test2';
Counter::reset($key1);
Counter::reset($key2);
$this->assertEquals(10, Counter::increment($key1, 10));
$this->assertEquals(10, Counter::increment($key2, 10));
$this->assertEquals(9, Counter::decrement($key1));
$this->assertEquals(8, Counter::decrement($key1));
$this->assertEquals(9, Counter::decrement($key2));
$this->assertEquals(8, Counter::decrement($key2));
$this->assertEquals(6, Counter::decrement($key2, 2));
$this->assertEquals(6, Counter::decrement($key1, 2));
$this->assertEquals(-2, Counter::decrement($key2, 8));
$this->assertEquals(-2, Counter::decrement($key1, 8));
}
}
<?php
namespace Phwoolcon\Tests\Integration\Util;
use Phwoolcon\Config;
use Phwoolcon\Db;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\Util\Counter;
class RdsCounterTest extends TestCase
{
public function setUp()
{
parent::setUp();
Config::set('counter.default', 'rds');
Counter::register($this->di);
$db = Db::connection();
$db->tableExists('counter') and $db->dropTable('counter');
}
public function tearDown()
{
Config::set('counter.default', 'auto');
parent::tearDown();
}
public function testIncrement()
{
$key1 = 'test';
$key2 = 'test2';
Counter::reset($key1);
Counter::reset($key2);
$this->assertEquals(1, Counter::increment($key1));
$this->assertEquals(2, Counter::increment($key1));
$this->assertEquals(1, Counter::increment($key2));
$this->assertEquals(2, Counter::increment($key2));
$this->assertEquals(4, Counter::increment($key2, 2));
$this->assertEquals(4, Counter::increment($key1, 2));
}
public function testDecrement()
{
$key1 = 'test';
$key2 = 'test2';
Counter::reset($key1);
Counter::reset($key2);
$this->assertEquals(10, Counter::increment($key1, 10));
$this->assertEquals(10, Counter::increment($key2, 10));
$this->assertEquals(9, Counter::decrement($key1));
$this->assertEquals(8, Counter::decrement($key1));
$this->assertEquals(9, Counter::decrement($key2));
$this->assertEquals(8, Counter::decrement($key2));
$this->assertEquals(6, Counter::decrement($key2, 2));
$this->assertEquals(6, Counter::decrement($key1, 2));
$this->assertEquals(-2, Counter::decrement($key2, 8));
$this->assertEquals(-2, Counter::decrement($key1, 8));
}
}
<?php
namespace Phwoolcon\Tests\Unit\Admin;
use Exception;
use Phalcon\Validation\Exception as ValidationException;
use Phwoolcon\Config;
use Phwoolcon\Tests\Helper\Admin\TestConfigTrait;
use Phwoolcon\Tests\Helper\TestCase;
class ConfigTraitTest extends TestCase
{
/**
* @var TestConfigTrait
*/
protected $configTrait;
protected function addTestConfigs()
{
Config::set('white_listed', [
'_white_list' => [
'foo',
],
'foo' => 'bar',
'hello' => 'world',
]);
Config::set('black_listed', [
'_black_list' => [
'foo',
],
'foo' => 'bar',
'hello' => 'world',
]);
Config::set('protected', [
'foo' => 'bar',
'hello' => 'world',
]);
}
public function setUp()
{
parent::setUp();
$this->configTrait = new TestConfigTrait();
$this->addTestConfigs();
}
public function testKeyList()
{
$list = $this->configTrait->keyList();
$this->assertArrayHasKey($whiteListed = 'white_listed', $list);
$this->assertArrayHasKey($blackListed = 'black_listed', $list);
$this->assertArrayNotHasKey($protected = 'protected', $list);
$this->assertNotEmpty(Config::get($whiteListed));
$this->assertNotEmpty(Config::get($blackListed));
$this->assertNotEmpty(Config::get($protected));
}
public function testGetCurrentConfig()
{
$key = 'white_listed';
$currentConfig = $this->configTrait->getCurrentConfig($key);
// foo should be visible because it is in white list
$this->assertEquals(fnGet($currentConfig, 'foo'), Config::get($key . '.foo'));
// hello should be invisible because it is not in white list
$this->assertNotEquals(fnGet($currentConfig, 'hello'), Config::get($key . '.hello'));
$key = 'black_listed';
$currentConfig = $this->configTrait->getCurrentConfig($key);
// foo should be invisible because it is in black list
$this->assertNotEquals(fnGet($currentConfig, 'foo'), Config::get($key . '.foo'));
// hello should be visible because it is not in black list
$this->assertEquals(fnGet($currentConfig, 'hello'), Config::get($key . '.hello'));
$e = false;
$key = 'protected';
try {
$this->configTrait->getCurrentConfig($key);
} catch (Exception $e) {
}
$this->assertInstanceOf(ValidationException::class, $e);
}
public function testFilterConfig()
{
$data = [
'foo' => 'baz',
'hello' => 'word',
];
$key = 'white_listed';
$filteredData = $this->configTrait->filterConfig($key, $data);
// foo should be kept because it is in white list
$this->assertEquals(fnGet($filteredData, 'foo'), fnGet($data, 'foo'));
// hello should be removed because it is not in white list
$this->assertNotEquals(fnGet($filteredData, 'hello'), fnGet($data, 'hello'));
$key = 'black_listed';
$filteredData = $this->configTrait->filterConfig($key, $data);
// foo should be removed because it is in black list
$this->assertNotEquals(fnGet($filteredData, 'foo'), fnGet($data, 'foo'));
// hello should be kept because it is not in black list
$this->assertEquals(fnGet($filteredData, 'hello'), fnGet($data, 'hello'));
$e = false;
$key = 'protected';
try {
$this->configTrait->filterConfig($key, $data);
} catch (Exception $e) {
}
$this->assertInstanceOf(ValidationException::class, $e);
}
public function testSubmitConfig()
{
$data = [
'foo' => 'baz',
'hello' => 'word',
];
$rawData = json_encode($data);
// Test white list
$key = 'white_listed';
$fooKey = $key . '.foo';
// Check foo value before submit
$this->assertEquals('bar', Config::get($fooKey));
$submittedData = $this->configTrait->submitConfig($key, $rawData);
// foo should be kept because it is in white list
$this->assertEquals(fnGet($submittedData, 'foo'), fnGet($data, 'foo'));
// hello should be removed because it is not in white list
$this->assertNotEquals(fnGet($submittedData, 'hello'), fnGet($data, 'hello'));
// Check foo value after submit
$this->reloadConfig();
$this->assertEquals('baz', Config::get($fooKey));
// Test remove db config
$this->addTestConfigs();
$this->configTrait->submitConfig($key, '');
$this->reloadConfig();
$this->assertEquals(null, Config::get($fooKey));
// Reset
$this->addTestConfigs();
// Test Black list
$key = 'black_listed';
$submittedData = $this->configTrait->submitConfig($key, $rawData);
// foo should be removed because it is in black list
$this->assertNotEquals(fnGet($submittedData, 'foo'), fnGet($data, 'foo'));
// hello should be kept because it is not in black list
$this->assertEquals(fnGet($submittedData, 'hello'), fnGet($data, 'hello'));
// Test undefined key
$e = false;
$key = 'protected';
try {
$this->configTrait->submitConfig($key, $rawData);
} catch (Exception $e) {
}
$this->assertInstanceOf(ValidationException::class, $e);
// Test bad data
$e = false;
$key = 'white_listed';
$badData = $rawData . 'oops';
try {
$this->configTrait->submitConfig($key, $badData);
} catch (Exception $e) {
}
$this->assertInstanceOf(ValidationException::class, $e);
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phwoolcon\Cache;
use Phwoolcon\Tests\Helper\TestCase;
class CacheTest extends TestCase
{
public function setUp()
{
parent::setUp();
Cache::register($this->di);
Cache::flush();
}
public function testSetAndRemoveCache()
{
$cacheKey = 'test.set.cache';
$value = str_repeat('Some stub', 1000);
Cache::set($cacheKey, $value);
$this->assertEquals($value, Cache::get($cacheKey), 'Unable to set cache');
Cache::delete($cacheKey);
$this->assertNull(Cache::get($cacheKey), 'Unable to delete cache');
$cacheKey = 'test.set.cache';
$value = 1234;
Cache::set($cacheKey, $value);
$this->assertEquals($value, Cache::get($cacheKey), 'Unable to set cache');
Cache::delete($cacheKey);
$this->assertNull(Cache::get($cacheKey), 'Unable to delete cache');
}
public function testCounter()
{
$cacheKey = 'test.counter';
Cache::set($cacheKey, 0);
$count = Cache::increment($cacheKey);
$this->assertEquals(1, $count);
$count = Cache::increment($cacheKey, 2);
$this->assertEquals(3, $count);
$count = Cache::decrement($cacheKey);
$this->assertEquals(2, $count);
$count = Cache::decrement($cacheKey, 2);
$this->assertEquals(0, $count);
}
public function testExistsAndKeys()
{
$cacheKey = 'test.exists';
$exists = Cache::exists($cacheKey);
$this->assertFalse($exists);
Cache::set($cacheKey, 'foo');
$exists = Cache::exists($cacheKey);
$this->assertTrue($exists);
// Bug in version < 3.0.2: query keys with prefix in file cache
if ($_SERVER['PHWOOLCON_PHALCON_VERSION'] >= 3000200) {
$keys = Cache::queryKeys($cacheKey);
} else {
$keys = Cache::queryKeys();
}
$this->assertNotEmpty($keys);
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phwoolcon\Cli;
use Phwoolcon\Tests\Helper\CliTestCase;
class CliTest extends CliTestCase
{
public function testGetConsoleWidth()
{
$this->assertTrue(is_numeric(Cli::getConsoleWidth()));
}
public function testCommandOutputFormat()
{
$output = $this->runCommand('test-command', ['question']);
$this->assertEquals('foo', trim($output));
}
public function testProgressBar()
{
$output = $this->runCommand('test-command', ['progress']);
$lines = explode("\n", trim($output));
$this->assertStringStartsWith('0/10', trim($lines[0]));
$this->assertContains('0%', trim($lines[0]));
$this->assertStringStartsWith('1/10', trim($lines[1]));
$this->assertContains('10%', trim($lines[1]));
$this->assertStringStartsWith('2/10', trim($lines[2]));
$this->assertContains('20%', trim($lines[2]));
}
public function testTimestampMessage()
{
$output = $this->runCommand('test-command', ['TimestampMessage']);
$this->assertStringStartsWith(date('[Y-m-d'), $output);
$this->assertStringEndsWith('foo', trim($output));
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Exception;
use Phalcon\Http\Response;
use Phwoolcon\Cookies;
use Phwoolcon\Exception\Http\CsrfException;
use Phwoolcon\Exception\Http\ForbiddenException;
use Phwoolcon\Exception\Http\NotFoundException;
use Phwoolcon\Router;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\Tests\Helper\TestController;
use Phwoolcon\View;
class ControllerTest extends TestCase
{
public function setUp()
{
parent::setUp();
Router::register($this->di);
View::register($this->di);
}
protected function getTestController()
{
$this->getView()->reset();
$controller = new TestController();
Cookies::reset();
$controller->response->resetHeaders()->setContent(null);
return $controller;
}
/**
* @return View
*/
protected function getView()
{
return $this->di->getShared('view');
}
public function testPageTitle()
{
$controller = $this->getTestController();
$controller->addPageTitle('Phwoolcon');
$controller->addPageTitle('Test Title');
$controller->render('test', 'controller-render');
$this->assertEquals('Test Title - Phwoolcon', View::getPageTitle());
}
public function testRender()
{
$this->getView()->reset();
$controller = $this->getTestController();
$controller->render('test', 'controller-render');
$this->assertEquals('TEST CONTROLLER RENDER', $controller->response->getContent());
}
public function testJsonReturn()
{
$controller = $this->getTestController();
$controller->testJsonReturn($data = [
'foo' => 'bar',
]);
$this->assertEquals(json_encode($data), $controller->response->getContent());
}
public function testInput()
{
$controller = $this->getTestController();
$oldRequest = $_REQUEST;
$_REQUEST[$key = 'foo'] = $value = 'bar';
$result = $controller->testInput($key);
$_REQUEST = $oldRequest;
$this->assertEquals($value, $result);
$this->assertNull($controller->testInput('non-existing'));
$this->assertEquals($value, $controller->testInput('non-existing', $value));
}
public function testRedirect()
{
$controller = $this->getTestController();
$controller->testRedirect($url = 'test-url');
$this->assertStringStartsWith('302', $controller->response->getHeaders()->get('Status'));
$this->assertEquals(url($url), $controller->response->getHeaders()->get('Location'));
}
public function testBrowserCache()
{
$controller = $this->getTestController();
$pageId = 'test-browser-cache';
$controller->response->setContent($content = 'Test browser cache');
$controller->testSetBrowserCache($pageId);
$cachedContent = $controller->testGetBrowserCache($pageId);
$eTag = fnGet($cachedContent, 'etag');
$this->assertEquals($content, fnGet($cachedContent, 'content'));
$this->assertEquals($eTag, $controller->response->getHeaders()->get('Etag'));
$cachedContentNonExisting = $controller->testGetBrowserCache('non-existing');
$this->assertNull(fnGet($cachedContentNonExisting, 'content'));
$controller2 = $this->getTestController();
$pageId2 = 'test-browser-cache-2';
$controller2->response->setContent($content2 = 'Test browser cache 2');
$cachedContent2 = $controller2->testGetBrowserCache('non-existing');
$this->assertNull(fnGet($cachedContent2, 'content'));
$controller2->testSetBrowserCache($pageId2, $controller2::BROWSER_CACHE_ETAG);
$eTag = $controller2->getBrowserCache($pageId2, $controller2::BROWSER_CACHE_ETAG);
$this->assertEquals($eTag, $controller2->response->getHeaders()->get('Etag'));
$controller2->testSetBrowserCache($pageId2, $controller2::BROWSER_CACHE_CONTENT);
$this->assertEquals($content2, $controller2->getBrowserCache($pageId2, $controller2::BROWSER_CACHE_CONTENT));
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phwoolcon\Cookies;
use Phwoolcon\Crypt;
use Phwoolcon\Http\Cookie;
use Phwoolcon\Tests\Helper\TestCase;
class CookiesTest extends TestCase
{
public function testSetAndGet()
{
Cookies::reset();
Cookies::set($name = 'foo', $value = 'bar');
$this->assertEquals($value, Cookies::get($name));
Cookies::reset();
}
public function testToArray()
{
Cookies::reset();
Cookies::set($name = 'hello', $value = 'world');
foreach (Cookies::toArray() as $cookie) {
$this->assertInstanceOf(Cookie::class, $cookie);
$this->assertEquals($name, $cookie->getName());
$this->assertEquals($value, $cookie->getValue());
break;
}
Cookies::reset();
}
public function testGetResponseValue()
{
Cookies::reset();
Cookies::set($name = 'hello', $value = 'world');
$cookie = Cookies::get($name);
$cookie->useEncryption(false);
$this->assertEquals($name, $cookie->getName());
$this->assertEquals($value, $cookie->getResponseValue());
$cookie->useEncryption(true);
$this->assertNotEquals($value, $encrypted = $cookie->getResponseValue());
/* @var \Phalcon\Crypt $crypt */
$crypt = $this->di->getShared('crypt');
$this->assertEquals($value, $crypt->decryptBase64($encrypted));
Cookies::reset();
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phwoolcon\Crypt;
use Phwoolcon\Tests\Helper\TestCase;
class CryptTest extends TestCase
{
public function setUp()
{
parent::setUp();
Crypt::reset();
}
public function testOpensslEncryptDecrypt()
{
$key = 'BnIv15w06qnvibLuuRfVSP9qb9MLPKFg';
$text = json_encode(['abc' => 'def', 'xxx1' => 'ooo', 'xxx2' => 'ooo', 'xxx3' => 'ooo', 'xxx4' => 'ooo']);
$encrypted = Crypt::opensslEncrypt($text, $key);
$this->assertNotEmpty($encrypted, 'Unable to encrypt');
$decrypted = Crypt::opensslDecrypt($encrypted, $key);
$this->assertNotEmpty($decrypted, 'Unable to decrypt');
$this->assertEquals($text, $decrypted, 'Bad decrypted text');
}
}
<?php
namespace Phwoolcon\Tests\Unit\Daemon;
use Phwoolcon\Db;
use Phwoolcon\Log;
use Phwoolcon\Tests\Helper\TestService as Service;
use Phwoolcon\Tests\Helper\TestCase;
use Swoole\Client;
use Swoole\Process;
class ServiceTest extends TestCase
{
/**
* @var Service
*/
protected $service;
protected function request($request)
{
$runDir = $this->service->getRunDir();
$port = include($runDir . 'service-port.php');
$sockFile = $runDir . 'service-' . $port . '.sock';
$client = new Client(SWOOLE_UNIX_STREAM);
$client->set([
'open_length_check' => true,
'package_length_type' => 'N',
'package_length_offset' => 0,
'package_body_offset' => 4,
]);
if (!@$client->connect($sockFile, 0, 20)) {
return false;
}
$request = serialize($request);
$client->send(pack('N', $length = strlen($request)));
if ($length > 2097152) {
foreach (str_split($request, 1048576) as $chunk) {
$client->send($chunk);
}
} else {
$client->send($request);
}
if ($rounds = $client->recv()) {
$roundsLength = unpack('N', $rounds)[1];
$rounds = substr($rounds, -$roundsLength);
$response = '';
for (; $rounds > 0; --$rounds) {
$responseChunk = $client->recv();
$chunkLength = unpack('N', $responseChunk)[1];
$response .= substr($responseChunk, -$chunkLength);
}
$client->close();
return unserialize($response);
}
return false;
}
public function setUp()
{
parent::setUp();
Service::register($this->di);
$this->service = $this->di->getShared('service');
$this->service->setTestCase($this);
}
public function tearDown()
{
$this->appendRemoteCoverage();
$this->service->stop();
$this->service->stop('old');
parent::tearDown();
}
public function testChoosePort()
{
$service = $this->service;
opcache_reset();
$port = $service->choosePort();
$this->assertEquals($port, $service->choosePort());
// Swap port
$service->choosePort(true);
opcache_reset();
$this->assertNotEquals($port, $service->choosePort());
// Swap port
$service->choosePort(true);
opcache_reset();
$this->assertEquals($port, $service->choosePort());
}
public function testStartAndStop()
{
$service = $this->service;
// Service not started, should be error 2 No such file or directory, or 111 Connection refused
$service->showStatus(null, false, $error);
$this->assertContains(fnGet($error, 'err'), [2, 111], fnGet($error, 'message'));
// Should be able to dry start
$this->assertTrue($service->start(true));
// Should be able to start
$serverProcess = $service->startIsolated();
// Should return running status
$status = $service->showStatus(null, false, $error);
$this->assertFalse($error);
$this->assertStringStartsWith('Service is running. PID: ' . $serverProcess->pid, $status);
// Should be able to stop
$service->stop();
// Service stopped, should be error 2 No such file or directory, or 111 Connection refused
$service->showStatus(null, false, $error);
$this->assertContains(fnGet($error, 'err'), [2, 111], fnGet($error, 'message'));
}
public function testReceive()
{
$service = $this->service;
// Should be able to start
$serverProcess = $service->startIsolated();
// Should return running status
$status = $service->showStatus(null, false, $error);
$this->assertFalse($error);
$this->assertStringStartsWith('Service is running. PID: ' . $serverProcess->pid, $status);
// Test request
$request = [
'request' => ['foo' => 'bar'],
'cookies' => [],
'server' => array_merge($_SERVER, ['REQUEST_URI' => '/test-controller-route', 'REQUEST_METHOD' => 'GET']),
'files' => [],
];
$response = $this->request($request);
$this->assertArrayHasKey('headers', $response);
$this->assertArrayHasKey('body', $response);
$this->assertArrayHasKey('meta', $response);
$this->assertEquals('HTTP/1.1 200 OK', $response['headers']['status']);
$this->assertEquals('Test Controllers Route Content', $response['body']);
// Test request for json api
$request = [
'request' => ['foo' => 'bar'],
'cookies' => [],
'server' => array_merge($_SERVER, ['REQUEST_URI' => '/api/test-json-api-data', 'REQUEST_METHOD' => 'GET']),
'files' => [],
];
$response = $this->request($request);
$this->assertArrayHasKey('headers', $response);
$this->assertArrayHasKey('body', $response);
$this->assertArrayHasKey('meta', $response);
$this->assertEquals('HTTP/1.1 200 OK', $response['headers']['status']);
$this->assertJson($response['body']);
// Test request for large body
$request = [
'request' => ['key' => $value = str_repeat('ABC', 1e6)],
'cookies' => [],
'server' => array_merge($_SERVER, ['REQUEST_URI' => '/test-controller-input', 'REQUEST_METHOD' => 'GET']),
'files' => [],
];
$response = $this->request($request);
$this->assertArrayHasKey('headers', $response);
$this->assertArrayHasKey('body', $response);
$this->assertArrayHasKey('meta', $response);
$this->assertEquals('HTTP/1.1 200 OK', $response['headers']['status']);
$this->assertEquals($value, $response['body']);
// Should be able to stop
$service->stop();
// Service stopped, should be error 2 No such file or directory, or 111 Connection refused
$service->showStatus(null, false, $error);
$this->assertContains(fnGet($error, 'err'), [2, 111], fnGet($error, 'message'));
}
public function testReload()
{
$service = $this->service;
// Should be able to start
$serverProcess = $service->startIsolated();
// Should return running status
$status = $service->showStatus(null, false, $error);
$this->assertFalse($error);
$this->assertStringStartsWith('Service is running. PID: ' . $serverProcess->pid, $status);
// Get current port
$oldPort = $service->choosePort();
// Port should be shifted
$service->shift();
$this->assertNotEquals($oldPort, $service->choosePort());
/*
* TODO The old instance should be stopped after new instance is started
* But I am unable to start two instances due to swoole status detection
* It may be implemented in the future if swoole fixed this
*/
// Should be able to stop old instance
$service->stop('old');
// Should be able to start new instance
$substituteServerProcess = $service->startIsolated();
// Should return running status
$status = $service->showStatus(null, false, $error);
$this->assertFalse($error);
$this->assertStringStartsWith('Service is running. PID: ' . $substituteServerProcess->pid, $status);
// Should be able to stop
$service->stop();
// Service stopped, should be error 2 No such file or directory, or 111 Connection refused
$service->showStatus(null, false, $error);
$this->assertContains(fnGet($error, 'err'), [2, 111], fnGet($error, 'message'));
}
}
<?php
namespace Phwoolcon\Tests\Unit\Util;
use LogicException;
use Phwoolcon\ErrorCodes;
use Phwoolcon\I18n;
use Phwoolcon\Tests\Helper\TestCase;
use UnexpectedValueException;
class ErrorCodesTest extends TestCase
{
public function setUp()
{
parent::setUp();
I18n::staticReset();
ErrorCodes::register($this->di);
}
public function testGetDetails()
{
list($code, $message) = ErrorCodes::getTestError();
$this->assertEquals('test_error', $code);
$this->assertEquals('Test error message', $message);
list($code, $message) = ErrorCodes::getTestParam('foo', 'bar');
$this->assertEquals('test_param', $code);
$this->assertEquals('Test error message with foo and bar', $message);
}
public function testGenerateException()
{
$exception = UnexpectedValueException::class;
$e = ErrorCodes::gen1234($exception);
$this->assertInstanceOf($exception, $e);
$this->assertEquals(1234, $e->getCode());
$this->assertEquals('Test numeric error code', $e->getMessage());
$exception = LogicException::class;
$e = ErrorCodes::genTestError($exception);
$this->assertInstanceOf($exception, $e);
$this->assertEquals(0, $e->getCode());
$this->assertEquals('Test error message [test_error]', $e->getMessage());
}
public function testNumericCodeWithAnnotation()
{
list($code, $message) = ErrorCodes::get2345WithAnnotation();
$this->assertEquals($expectedCode = '2345', $code);
$this->assertEquals($expectedMessage = 'Test numeric error code with annotation', $message);
$exception = LogicException::class;
$e = ErrorCodes::gen2345WithAnnotation($exception);
$this->assertInstanceOf($exception, $e);
$this->assertEquals($expectedCode, $e->getCode());
$this->assertEquals($expectedMessage, $e->getMessage());
}
public function testIdeHelperGenerator()
{
$ideHelper = ErrorCodes::ideHelperGenerator();
// echo PHP_EOL, $ideHelper, PHP_EOL;
// exit;
$this->assertEquals(<<<'METHOD'
public static function getTestError() {
return ['test_error', 'Test error message'];
}
public static function genTestError($exception) {
return new $exception('Test error message [test_error]', 0);
}
public static function getTestParam($param, $anotherParam) {
return ['test_param', 'Test error message with %param% and %another_param%'];
}
public static function genTestParam($exception, $param, $anotherParam) {
return new $exception('Test error message with %param% and %another_param% [test_param]', 0);
}
public static function get1234() {
return ['1234', 'Test numeric error code'];
}
public static function gen1234($exception) {
return new $exception('Test numeric error code', 1234);
}
public static function get2345WithAnnotation() {
return ['2345', 'Test numeric error code with annotation'];
}
public static function gen2345WithAnnotation($exception) {
return new $exception('Test numeric error code with annotation', 2345);
}
METHOD
, $ideHelper);
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phalcon\Events\Event;
use Phwoolcon\Events;
use Phwoolcon\Tests\Helper\TestCase;
class EventsTest extends TestCase
{
protected $eventChangeValue = false;
public function setUp()
{
parent::setUp();
}
public function testFireAndCatchEvent()
{
Events::attach($eventType = 'test:fireAndCatchEvent', function (Event $event) {
/* @var static $obj */
$obj = $event->getSource();
$obj->eventChangeValue = true;
});
Events::fire($eventType, $this);
$this->assertTrue($this->eventChangeValue, 'Event not caught');
}
public function testDetachEvent()
{
Events::detachAll($eventType = 'test:fireAndCatchEvent');
Events::attach($eventType, $handler = function (Event $event) {
/* @var static $obj */
$obj = $event->getSource();
$obj->eventChangeValue = true;
});
$this->eventChangeValue = false;
Events::detach($eventType, $handler);
Events::fire($eventType, $this);
$this->assertFalse($this->eventChangeValue, 'Event not detached');
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phwoolcon\Config;
use Phwoolcon\Tests\Helper\TestCase;
class FunctionsTest extends TestCase
{
public function setUp()
{
parent::setUp();
}
public function testArrayForget()
{
$array = [
'child' => [
'child' => [
'child' => 'no child',
],
],
];
$this->assertTrue(isset($array['child']['child']['child']), 'Bad initial array');
array_forget($array, 'child.child.child');
$this->assertTrue(!isset($array['child']['child']['child']), 'Unable to forget array key');
$this->assertTrue(isset($array['child']['child']), 'Bad forget array result');
array_forget($array, 'child.child.child.child');
}
public function testArraySet()
{
$array = [];
$value = 'no child';
$this->assertTrue(!isset($array['child']), 'Bad initial array');
array_set($array, 'child.child.child', $value);
$this->assertTrue(isset($array['child']['child']['child']), 'Unable to set array key');
$this->assertEquals($value, $array['child']['child']['child'], 'Bad array key set result');
array_set($array, null, $value);
$this->assertEquals($value, $array, 'Bad array set result');
}
public function testBase62encode()
{
$this->assertEquals('Z', base62encode('61'), 'Bad base62encode value for 61');
$this->assertEquals('10', base62encode('62'), 'Bad base62encode value for 62');
}
public function testFnGet()
{
$array = [
'child' => [
'child' => [
'child' => 'no child',
],
],
];
$obj = json_decode(json_encode($array));
$default = 'default';
$this->assertEquals(
$array['child']['child']['child'],
fnGet($array, 'child.child.child'),
'Unable to fetch child on array'
);
$this->assertEquals(
$array['child']['child']['child'],
fnGet($array, 'child.child.child', null, '.', true),
'Unable to fetch child on array'
);
$this->assertEquals(
$obj->child->child->child,
fnGet($obj, 'child.child.child', null, '.', true),
'Unable to fetch child on object'
);
$this->assertEquals(
$default,
fnGet($array, 'bad.key', $default),
'Unable to return default value if child not found on array'
);
$this->assertEquals(
$default,
fnGet($obj, 'bad.key', $default, '.', true),
'Unable to return default value if child not found on object'
);
}
public function testUrl()
{
$baseUrl = Config::get('app.url');
$uri = 'test/done';
$expected = rtrim($baseUrl, '/') . '/' . ltrim($uri, '/');
$this->assertEquals($expected, url($uri), 'Bad url() result');
$expected = rtrim($baseUrl, '/') . '/' . ltrim($uri, '/') . '?k=v';
$this->assertEquals($expected, url($uri, ['k' => 'v']), 'Bad url() result on queries');
Config::set('app.enable_https', true);
$expected = str_replace('http:', 'https:', rtrim($baseUrl, '/')) . '/' . ltrim($uri, '/') . '?k=v';
$this->assertEquals($expected, secureUrl($uri, ['k' => 'v']), 'Bad url() result on https');
$expected = $uri = 'http://test.com';
$this->assertEquals($expected, url($uri), 'Bad url() result on external links');
}
public function testGetRelativePath()
{
// Same level
$source = '/a/b/c.php';
$destination = '/a/b/d.php';
$expected = './d.php';
$this->assertEquals($expected, getRelativePath($source, $destination));
// To child
$source = '/a/b/c.php';
$destination = '/a/b/c/d.php';
$expected = './c/d.php';
$this->assertEquals($expected, getRelativePath($source, $destination));
// Another child
$source = '/a/b/c.php';
$destination = '/a/b/d/d.php';
$expected = './d/d.php';
$this->assertEquals($expected, getRelativePath($source, $destination));
// Just parent
$source = '/a/b/c.php';
$destination = '/a/c.php';
$expected = '../c.php';
$this->assertEquals($expected, getRelativePath($source, $destination));
// Different parent
$source = '/a/b/c.php';
$destination = '/a/c/d.php';
$expected = '../c/d.php';
$this->assertEquals($expected, getRelativePath($source, $destination));
// Directories
$source = '/a/b/c/';
$destination = '/a/b/d/';
$expected = './d/';
$this->assertEquals($expected, getRelativePath($source, $destination));
// Directories
$source = '/a/b/c/';
$destination = '/a/c/d/';
$expected = '../c/d/';
$this->assertEquals($expected, getRelativePath($source, $destination));
// Non absolute paths
$source = 'a/b/c/';
$destination = 'a/c/d/';
$expected = $destination;
$this->assertEquals($expected, getRelativePath($source, $destination));
}
public function testRemoveDir()
{
// Prepare dir structure
$targetDir = storagePath('dirs/rand-' . mt_rand(1000, 9999));
mkdir($subDir = $targetDir . '/a/b/c', 0777, true);
touch($file = $subDir . '/x.log');
$this->assertFileExists($file);
// Remove dir recursively
removeDir(dirname($targetDir));
$this->assertFileNotExists($targetDir);
}
public function testCopyDirMerge()
{
// Prepare variables
$sourceDir = storagePath('dirs/source-' . mt_rand(1000, 9999));
$targetDir = storagePath('dirs/target-' . mt_rand(1000, 9999));
$subDir = '/a/b/c/';
$file1 = $fileContent1 = 'x.log';
$file2 = $fileContent2 = 'y.log';
$file3 = $fileContent3 = 'z.log';
$sourceSubDir = $sourceDir . $subDir;
$sourceFile1 = $sourceSubDir . $file1;
$sourceFile2 = $sourceSubDir . $file2;
$sourceFile3 = $sourceSubDir . $file3;
$targetSubDir = $targetDir . $subDir;
$targetFile1 = $targetSubDir . $file1;
$targetFile2 = $targetSubDir . $file2;
$targetFile3 = $targetSubDir . $file3;
// Prepare source dir structure, with file1, file2 and file3
mkdir($sourceSubDir, 0777, true);
fileSaveArray($sourceFile1, $fileContent1);
fileSaveArray($sourceFile2, $fileContent2);
fileSaveArray($sourceFile3, $fileContent3);
$this->assertFileExists($sourceFile1);
$this->assertFileExists($sourceFile2);
$this->assertFileExists($sourceFile3);
// Prepare target dir structure, with file3 which holds content 'a.log'
mkdir($targetSubDir, 0777, true);
fileSaveArray($targetFile3, $fileContent3Target = 'a.log');
$this->assertFileExists($targetFile3);
// Target don't have file1 and file2 before copy
$this->assertFileNotExists($targetFile1);
$this->assertFileNotExists($targetFile2);
// Perform copy
copyDirMerge($sourceDir, $targetDir);
// Now target have file1, file2 and file3, but file3 remains unchanged
$this->assertFileExists($targetFile1);
$this->assertFileExists($targetFile2);
$this->assertFileExists($targetFile3);
$this->assertEquals($fileContent1, include $sourceFile1);
$this->assertEquals($fileContent2, include $sourceFile2);
$this->assertEquals($fileContent3, include $sourceFile3);
$this->assertEquals($fileContent1, include $targetFile1);
$this->assertEquals($fileContent2, include $targetFile2);
$this->assertNotEquals($fileContent3, include $targetFile3);
$this->assertEquals($fileContent3Target, include $targetFile3);
// Clean
removeDir(dirname($sourceDir));
$this->assertFileNotExists($sourceDir);
$this->assertFileNotExists($targetDir);
}
public function testCopyDirOverride()
{
// Prepare variables
$sourceDir = storagePath('dirs/source-' . mt_rand(1000, 9999));
$targetDir = storagePath('dirs/target-' . mt_rand(1000, 9999));
$subDir = '/a/b/c/';
$file1 = $fileContent1 = 'x.log';
$file2 = $fileContent2 = 'y.log';
$file3 = $fileContent3 = 'z.log';
$sourceSubDir = $sourceDir . $subDir;
$sourceFile1 = $sourceSubDir . $file1;
$sourceFile2 = $sourceSubDir . $file2;
$sourceFile3 = $sourceSubDir . $file3;
$targetSubDir = $targetDir . $subDir;
$targetFile1 = $targetSubDir . $file1;
$targetFile2 = $targetSubDir . $file2;
$targetFile3 = $targetSubDir . $file3;
// Prepare source dir structure, with file1, file2 and file3
mkdir($sourceSubDir, 0777, true);
fileSaveArray($sourceFile1, $fileContent1);
fileSaveArray($sourceFile2, $fileContent2);
fileSaveArray($sourceFile3, $fileContent3);
$this->assertFileExists($sourceFile1);
$this->assertFileExists($sourceFile2);
$this->assertFileExists($sourceFile3);
// Prepare target dir structure, with file3 which holds content 'a.log'
mkdir($targetSubDir, 0777, true);
fileSaveArray($targetFile3, $fileContent3Target = 'a.log');
$this->assertFileExists($targetFile3);
// Target don't have file1 and file2 before copy
$this->assertFileNotExists($targetFile1);
$this->assertFileNotExists($targetFile2);
// Perform copy
copyDirOverride($sourceDir, $targetDir);
// Now target have file1, file2 and file3, this time file3 is overridden
$this->assertFileExists($targetFile1);
$this->assertFileExists($targetFile2);
$this->assertFileExists($targetFile3);
$this->assertEquals($fileContent1, include $sourceFile1);
$this->assertEquals($fileContent2, include $sourceFile2);
$this->assertEquals($fileContent3, include $sourceFile3);
$this->assertEquals($fileContent1, include $targetFile1);
$this->assertEquals($fileContent2, include $targetFile2);
$this->assertEquals($fileContent3, include $targetFile3);
$this->assertNotEquals($fileContent3Target, include $targetFile3);
// Clean
removeDir(dirname($sourceDir));
$this->assertFileNotExists($sourceDir);
$this->assertFileNotExists($targetDir);
}
public function testCopyDirReplace()
{
// Prepare variables
$sourceDir = storagePath('dirs/source-' . mt_rand(1000, 9999));
$targetDir = storagePath('dirs/target-' . mt_rand(1000, 9999));
$subDir = '/a/b/c/';
$file1 = $fileContent1 = 'x.log';
$file2 = $fileContent2 = 'y.log';
$file3 = $fileContent3 = 'z.log';
$file4 = $fileContent4 = 'a.log';
$sourceSubDir = $sourceDir . $subDir;
$sourceFile1 = $sourceSubDir . $file1;
$sourceFile2 = $sourceSubDir . $file2;
$sourceFile4 = $sourceSubDir . $file4;
$targetSubDir = $targetDir . $subDir;
$targetFile1 = $targetSubDir . $file1;
$targetFile2 = $targetSubDir . $file2;
$targetFile3 = $targetSubDir . $file3;
$targetFile4 = $targetSubDir . $file4;
// Prepare source dir structure, with file1, file2 and file4
mkdir($sourceSubDir, 0777, true);
fileSaveArray($sourceFile1, $fileContent1);
fileSaveArray($sourceFile2, $fileContent2);
fileSaveArray($sourceFile4, $fileContent4);
$this->assertFileExists($sourceFile1);
$this->assertFileExists($sourceFile2);
$this->assertFileExists($sourceFile4);
// Prepare target dir structure, with file3
mkdir($targetSubDir, 0777, true);
fileSaveArray($targetFile3, $fileContent3);
$this->assertFileExists($targetFile3);
// Target don't have file1, file2 and file4 before copy
$this->assertFileNotExists($targetFile1);
$this->assertFileNotExists($targetFile2);
$this->assertFileNotExists($targetFile4);
// Perform copy
copyDirReplace($sourceDir, $targetDir);
// Now target have file1, file2 and file4, file3 is removed
$this->assertFileExists($targetFile1);
$this->assertFileExists($targetFile2);
$this->assertFileExists($targetFile4);
$this->assertFileNotExists($targetFile3);
$this->assertEquals($fileContent1, include $sourceFile1);
$this->assertEquals($fileContent2, include $sourceFile2);
$this->assertEquals($fileContent4, include $sourceFile4);
$this->assertEquals($fileContent1, include $targetFile1);
$this->assertEquals($fileContent2, include $targetFile2);
$this->assertEquals($fileContent4, include $targetFile4);
// Clean
removeDir(dirname($sourceDir));
$this->assertFileNotExists($sourceDir);
$this->assertFileNotExists($targetDir);
}
public function testSymlinkDirOverride()
{
// Prepare variables
$sourceDir = storagePath('dirs/source-' . mt_rand(1000, 9999));
$targetDir = storagePath('dirs/target-' . mt_rand(1000, 9999));
$subDir = '/a/b/c/';
$file1 = $fileContent1 = 'x.log';
$file2 = $fileContent2 = 'y.log';
$file3 = $fileContent3 = 'z.log';
$file4 = $fileContent4 = 'a.log';
$sourceSubDir = $sourceDir . $subDir;
$sourceFile1 = $sourceSubDir . $file1;
$sourceFile2 = $sourceSubDir . $file2;
$sourceFile3 = $sourceSubDir . $file3;
$targetSubDir = $targetDir . $subDir;
$targetFile1 = $targetSubDir . $file1;
$targetFile2 = $targetSubDir . $file2;
$targetFile3 = $targetSubDir . $file3;
$targetFile4 = $targetSubDir . $file4;
// Prepare source dir structure, with file1, file2 and file3
mkdir($sourceSubDir, 0777, true);
fileSaveArray($sourceFile1, $fileContent1);
fileSaveArray($sourceFile2, $fileContent2);
fileSaveArray($sourceFile3, $fileContent3);
$this->assertFileExists($sourceFile1);
$this->assertFileExists($sourceFile2);
$this->assertFileExists($sourceFile3);
// Prepare target dir structure, with file3 which holds content 'a.log' and file4
mkdir($targetSubDir, 0777, true);
fileSaveArray($targetFile3, $fileContent3Target = 'a.log');
fileSaveArray($targetFile4, $fileContent4);
$this->assertFileExists($targetFile3);
$this->assertFileExists($targetFile4);
// Target don't have file1 and file2 before copy
$this->assertFileNotExists($targetFile1);
$this->assertFileNotExists($targetFile2);
// Perform symlink
symlinkDirOverride($sourceDir, $targetDir);
// Now target have symlinks to file1, file2, file3 and file4, file3 is overridden, but file4 remain unchanged
$this->assertFileExists($targetFile1);
$this->assertFileExists($targetFile2);
$this->assertFileExists($targetFile3);
$this->assertFileExists($targetFile4);
$this->assertTrue(is_link($targetFile1));
$this->assertTrue(is_link($targetFile2));
$this->assertTrue(is_link($targetFile3));
$this->assertFalse(is_link($targetFile4));
// Check link path
$basedir = dirname($sourceDir);
$this->assertEquals(str_replace($basedir, '../../../..', $sourceFile1), readlink($targetFile1));
$this->assertEquals(str_replace($basedir, '../../../..', $sourceFile2), readlink($targetFile2));
$this->assertEquals(str_replace($basedir, '../../../..', $sourceFile3), readlink($targetFile3));
// Check source contents
$this->assertEquals($fileContent1, include $sourceFile1);
$this->assertEquals($fileContent2, include $sourceFile2);
$this->assertEquals($fileContent3, include $sourceFile3);
// Check target contents
$this->assertEquals($fileContent1, include $targetFile1);
$this->assertEquals($fileContent2, include $targetFile2);
$this->assertEquals($fileContent3, include $targetFile3);
$this->assertNotEquals($fileContent3Target, include $targetFile3);
$this->assertEquals($fileContent4, include $targetFile4);
// Clean
removeDir(dirname($sourceDir));
$this->assertFileNotExists($sourceDir);
$this->assertFileNotExists($targetDir);
}
public function testFileSaveInclude()
{
// Prepare files
$targetDir = storagePath('dirs/save-' . mt_rand(1000, 9999));
mkdir($targetDir, 0777, true);
$targetFile = $targetDir . '/include-me.php';
$fileToBeIncluded = $targetDir . '/to-be-included.php';
$x = $oldX = 456;
$y = $newX = 789;
file_put_contents($fileToBeIncluded, '<?php $x = ' . var_export($y, true) . ';');
$this->assertFileExists($fileToBeIncluded);
// Perform save
fileSaveInclude($targetFile, [$fileToBeIncluded]);
// Check saved file
$this->assertFileExists($targetFile);
// Replace ROOT_PATH with TEST_ROOT_PATH
file_put_contents($targetFile, str_replace(' ROOT_PATH ', ' TEST_ROOT_PATH ', file_get_contents($targetFile)));
// Check saved content
$this->assertEquals($oldX, $x);
include $targetFile;
$this->assertEquals($newX, $x);
// Clean
removeDir(dirname($targetDir));
$this->assertFileNotExists($targetFile);
$this->assertFileNotExists($fileToBeIncluded);
}
public function testDetectPhwoolconPackageFiles()
{
$baseDir = dirname(dirname(dirname(__DIR__)));
$packageFiles = detectPhwoolconPackageFiles(dirname($baseDir));
$target = $baseDir . '/phwoolcon/phwoolcon-package/package.php';
$this->assertContains($target, $packageFiles);
}
public function testEscape()
{
$text = '<script>alert(1);</script>';
$expected = '&lt;script&gt;alert(1);&lt;/script&gt;';
$this->assertEquals($expected, _e($text));
}
public function testArraySortedMerge()
{
$array = [
10 => [ // 10 is a sort order
'foo' => 'bar', // Holds value 'bar' in key 'foo'
'who' => 'me',
],
20 => [ // 20 is a bigger sort order
'foo' => 'baz', // This will override the key 'foo' with value 'baz'
'hello' => 'world', // New values will be merged
],
];
$expected = [
'foo' => 'baz',
'who' => 'me',
'hello' => 'world',
];
$this->assertEquals($expected, arraySortedMerge($array));
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phwoolcon\Cache\Clearer;
use Phwoolcon\Config;
use Phwoolcon\Cookies;
use Phwoolcon\I18n;
use Phwoolcon\Session;
use Phwoolcon\Tests\Helper\TestCase;
class I18nTest extends TestCase
{
protected $eventChangeValue = false;
public function setUp()
{
parent::setUp();
Clearer::clear(Clearer::TYPE_LOCALE);
}
public function testTranslate()
{
I18n::clearCache();
I18n::staticReset();
$this->assertEquals('测试', __('Test'), 'Bad translate result');
$this->assertEquals('测试1', __('Test', [], 'another-package'), 'Bad translate result with package');
$this->assertEquals($someString = 'Some string', __($someString), 'Bad translate result with undefined string');
$this->assertEquals('Some words', __('Some %stuff%', ['stuff' => 'words']), 'Bad translate result with params');
}
public function testAvailableLocales()
{
$this->assertEquals(Config::get('i18n.available_locales'), I18n::getAvailableLocales(), 'Bad locales list');
}
public function testSetLocale()
{
Session::start();
$currentLocale = I18n::getCurrentLocale();
$_REQUEST['_locale'] = 'en';
I18n::staticReset();
$this->assertEquals('en', I18n::getCurrentLocale(), 'Unable to set locale via request');
$_REQUEST['_locale'] = $currentLocale;
I18n::staticReset();
unset($_REQUEST['_locale']);
I18n::setLocale('en');
$this->assertEquals('en', I18n::getCurrentLocale(), 'Unable to set locale via setLocale()');
I18n::setLocale($currentLocale);
$this->assertEquals($currentLocale, I18n::getCurrentLocale(), 'Unable to set locale via setLocale()');
I18n::setLocale('non-existing');
$this->assertEquals(
$currentLocale,
I18n::getCurrentLocale(),
'Unable to restore default locale via setLocale()'
);
Session::end();
I18n::clearCache(true);
Cookies::set('locale', 'en');
Session::start();
I18n::staticReset();
$this->assertEquals('en', I18n::getCurrentLocale(), 'Unable to set locale via cookie');
$_REQUEST['_locale'] = $currentLocale;
I18n::staticReset();
unset($_REQUEST['_locale']);
Session::set('current_locale', 'en');
I18n::staticReset();
$this->assertEquals('en', I18n::getCurrentLocale(), 'Unable to set locale via session');
Session::set('current_locale', null);
I18n::staticReset();
$this->assertEquals($currentLocale, I18n::getCurrentLocale(), 'Unable to restore default locale');
Session::end();
}
public function testDisableMultiLocale()
{
I18n::useMultiLocale(false);
$currentLocale = I18n::getCurrentLocale();
$_REQUEST['_locale'] = 'en';
I18n::staticReset();
$this->assertEquals($currentLocale, I18n::getCurrentLocale(), 'Unable to disable multi locale');
$_REQUEST['_locale'] = 'zh_CN';
I18n::staticReset();
$this->assertEquals($currentLocale, I18n::getCurrentLocale(), 'Unable to disable multi locale');
unset($_REQUEST['_locale']);
I18n::useMultiLocale(true);
}
public function testCheckMobile()
{
$mobile = '13579246801';
$this->assertTrue(I18n::checkMobile($mobile, 'CN'), 'Bad check mobile result');
}
public function testFormatPrice()
{
$amount = '1234.5';
$this->assertEquals('¥1,234.50', price($amount));
}
}
<?php
namespace Phwoolcon\Tests\Unit\Model;
use Phwoolcon\Model\DynamicTrait\EmptyTrait;
use Phwoolcon\Model\DynamicTrait\Loader;
use Phwoolcon\Tests\Helper\Model\TestDynamicTrait;
use Phwoolcon\Tests\Helper\TestCase;
class DynamicTraitTest extends TestCase
{
public function setUp()
{
parent::setUp();
Loader::register($this->di);
}
public function testLoadTrait()
{
$model = new TestDynamicTrait();
$traits = class_uses($model);
$this->assertArrayHasKey(EmptyTrait::class, $traits);
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use ErrorException;
use Exception;
use Phwoolcon\Payload;
use Phwoolcon\Tests\Helper\TestCase;
class PayloadTest extends TestCase
{
public function setUp()
{
parent::setUp();
}
public function testCreate()
{
$data = [
'foo' => 'bar',
];
$payload = Payload::create($data);
$this->assertEquals($data, $payload->getData());
}
public function testSetData()
{
$data = [
'foo' => 'bar',
];
$payload = Payload::create([]);
$payload->setData($data);
$this->assertEquals($data, $payload->getData());
$payload->setData($key = 'hello', $value = 'world');
$this->assertEquals($value, $payload->getData($key));
}
public function testHasData()
{
$data = [
'foo' => 'bar',
];
$payload = Payload::create($data);
$this->assertTrue($payload->hasData('foo'));
$this->assertFalse($payload->hasData('non-exists'));
}
public function testMagicCallSetAndGet()
{
$payload = Payload::create([]);
$this->assertNull($payload->getFoo());
$payload->setFoo($value = 'bar');
$this->assertEquals($value, $payload->getFoo());
}
public function testBadMagicCall()
{
$payload = Payload::create([]);
$e = null;
try {
$payload->hello();
} catch (Exception $e) {
}
$this->assertInstanceOf(ErrorException::class, $e);
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Exception;
use Phalcon\Http\Response;
use Phwoolcon\Cache\Clearer;
use Phwoolcon\Config;
use Phwoolcon\Cookies;
use Phwoolcon\Exception\Http\CsrfException;
use Phwoolcon\Exception\Http\ForbiddenException;
use Phwoolcon\Exception\Http\NotFoundException;
use Phwoolcon\Log;
use Phwoolcon\Router;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\View;
class RouterTest extends TestCase
{
public function setUp()
{
parent::setUp();
Router::register($this->di);
View::register($this->di);
}
public function tearDown()
{
unset($_SERVER['REQUEST_METHOD']);
parent::tearDown();
}
/**
* @return Router
*/
protected function getRouter()
{
return $this->di->getShared('router');
}
/**
* @return View
*/
protected function getView()
{
return $this->di->getShared('view');
}
/**
* @param string $uri
* @param string $method
* @return CsrfException|NotFoundException|Response
*/
protected function dispatch($uri, $method = 'GET')
{
$_SERVER['REQUEST_METHOD'] = $method;
$router = $this->getRouter();
$router::staticReset();
Cookies::reset();
$this->getView()->reset();
try {
return $router->dispatch($uri);
} catch (Exception $e) {
Log::exception($e);
return $e;
}
}
public function test404Routes()
{
$response = $this->dispatch('/404');
$this->assertInstanceOf(NotFoundException::class, $response);
$this->assertEquals('404 NOT FOUND', $response->toResponse()->getContent());
$this->assertFalse($this->getView()->isAdmin());
}
public function testClosureRoutes()
{
$response = $this->dispatch('/test-closure-route');
$this->assertInstanceOf(Response::class, $response);
$this->assertContains('Test Closure Route Content', $response->getContent());
$this->assertFalse($this->getView()->isAdmin());
}
public function testControllerRoutes()
{
$response = $this->dispatch('/test-controller-route');
$this->assertInstanceOf(Response::class, $response);
$this->assertContains('Test Controllers Route Content', $response->getContent());
$this->assertFalse($this->getView()->isAdmin());
}
public function testRegexRoutes()
{
$expected = var_export([
$what = 'regex',
'what' => $what,
], 1);
$response = $this->dispatch("/test/{$what}/regex-route");
$this->assertEquals($expected, $response->getContent());
$expected = var_export([
$what = 'foo',
'what' => $what,
], 1);
$response = $this->dispatch("/test/{$what}/regex-route");
$this->assertEquals($expected, $response->getContent());
}
public function testFilteredRoutes()
{
$response = $this->dispatch('/test-filtered-route', 'POST');
$this->assertInstanceOf(Response::class, $response);
$this->assertContains('Test Controllers Route Content', $response->getContent());
$this->assertFalse($this->getView()->isAdmin());
}
public function testFailedFilterRoutes()
{
$response = $this->dispatch('/test-failed-filter-route', 'POST');
$this->assertInstanceOf(NotFoundException::class, $response);
$this->assertEquals('404 NOT FOUND', $response->toResponse()->getContent());
$this->assertFalse($this->getView()->isAdmin());
}
public function testExceptionFilterRoutes()
{
$response = $this->dispatch('/test-exception-filter-route', 'GET');
$this->assertInstanceOf(ForbiddenException::class, $response);
$response = $response->toResponse();
$this->assertEquals('ALWAYS EXCEPTION', $response->getContent());
$this->assertEquals('bar', $response->getHeaders()->get('foo'));
$this->assertFalse($this->getView()->isAdmin());
}
public function testCsrfCheck()
{
$response = $this->dispatch('/test-csrf-check', 'POST');
$this->assertInstanceOf(CsrfException::class, $response);
$this->assertEquals('403 FORBIDDEN', $response->toResponse()->getContent());
$this->assertFalse($this->getView()->isAdmin());
}
public function testPrefixedRoutes()
{
$response = $this->dispatch('/prefix/test-route');
$this->assertInstanceOf(Response::class, $response);
$this->assertContains('Test Prefixed Route Content', $response->getContent());
$this->assertFalse($this->getView()->isAdmin());
}
public function testCallableRoutes()
{
$response = $this->dispatch('/prefix/test-callable-route');
$this->assertInstanceOf(Response::class, $response);
$this->assertContains('Test Prefixed Route Content', $response->getContent());
$this->assertFalse($this->getView()->isAdmin());
}
public function testAdminRoutes()
{
$response = $this->dispatch('/admin/test-route');
$this->assertInstanceOf(Response::class, $response);
$this->assertContains('Test Admin Route Content', $response->getContent());
$this->assertTrue($this->getView()->isAdmin());
}
public function testApiRoutes()
{
$response = $this->dispatch('/api/test-route');
$this->assertInstanceOf(Response::class, $response);
$this->assertContains('Test Api Route Content', $response->getContent());
$this->assertFalse($this->getView()->isAdmin());
}
public function testApiDataRoutes()
{
$response = $this->dispatch('/api/test-json-api-data');
$this->assertInstanceOf(Response::class, $response);
$content = $response->getContent();
$this->assertContains(json_encode([
'id' => 1,
'type' => 'entity',
'attributes' => [
'foo' => 'bar',
],
]), $content);
$this->assertContains('jsonapi', $content);
$this->assertFalse($this->getView()->isAdmin());
}
public function testApiErrorRoutes()
{
$response = $this->dispatch('/api/test-json-api-error');
$this->assertInstanceOf(Response::class, $response);
$content = $response->getContent();
$this->assertContains(json_encode([
[
'code' => 'foo',
'title' => 'bar',
],
]), $content);
$this->assertContains('jsonapi', $content);
$this->assertFalse($this->getView()->isAdmin());
}
public function testApiMetaRoutes()
{
$response = $this->dispatch('/api/test-json-api-meta');
$this->assertInstanceOf(Response::class, $response);
$content = $response->getContent();
$this->assertContains(json_encode([
'meta_foo' => 'bar',
]), $content);
$this->assertContains('jsonapi', $content);
$this->assertFalse($this->getView()->isAdmin());
}
public function testApi404Routes()
{
$response = $this->dispatch('/api/404');
$this->assertInstanceOf(NotFoundException::class, $response);
$content = $response->toResponse()->getContent();
$this->assertContains(json_encode([
'status' => 404,
'code' => 404,
'title' => '404 Not Found',
]), $content);
$this->assertContains('jsonapi', $content);
$this->assertFalse($this->getView()->isAdmin());
}
public function testGenerateErrorPage()
{
$this->getView()->reset();
$this->assertEquals('404 NOT FOUND', Router::generateErrorPage('404', '404 PAGE TITLE'));
}
public function testCachedRoutes()
{
Config::set('app.cache_routes', true);
Clearer::clear(Clearer::TYPE_ROUTES);
Router::register($this->di);
$this->testClosureRoutes();
Clearer::clear(Clearer::TYPE_ROUTES);
Config::set('app.cache_routes', false);
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phwoolcon\Security;
use Phwoolcon\Tests\Helper\TestCase;
class SecurityTest extends TestCase
{
public function setUp()
{
parent::setUp();
}
public function testSignArrayMd5()
{
$data = ['foo' => 'bar'];
$secret = 'some-stuff';
$sign = Security::signArrayMd5($data, $secret);
$this->assertEquals($sign, Security::signArrayMd5($data, $secret));
}
public function testSignArraySha256()
{
$data = ['foo' => 'bar'];
$secret = 'some-stuff';
$sign = Security::signArraySha256($data, $secret);
$this->assertEquals($sign, Security::signArraySha256($data, $secret));
}
public function testSignArrayHmacSha256()
{
$data = ['foo' => 'bar'];
$secret = 'some-stuff';
$sign = Security::signArrayHmacSha256($data, $secret);
$this->assertEquals($sign, Security::signArrayHmacSha256($data, $secret));
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phwoolcon\Config;
use Phwoolcon\Cookies;
use Phwoolcon\Session;
use Phwoolcon\Tests\Helper\TestCase;
class SessionTest extends TestCase
{
public function setUp()
{
parent::setUp();
}
protected function realTestSessionCRUD($driver)
{
Config::set('session.default', $driver);
Session::register($this->di);
$suffix = " ({$driver})";
Session::flush();
// Start session
$this->assertFalse(isset($_SESSION), '$_SESSION should not exist before session start' . $suffix);
$this->assertTrue(Session::start(), 'Failed to start session' . $suffix);
$this->assertFalse(Session::start(), 'Should not start duplicated sessions' . $suffix);
// Get session id
$this->assertNotEmpty($sid = Session::getId(), 'Session id not generated');
// Session detection
$this->assertTrue(isset($_SESSION), '$_SESSION should exist after session start' . $suffix);
$this->assertFalse(isset($_SESSION[$key = 'test_key']), 'Session value should not exist before set' . $suffix);
// Set session value
Session::set($key, $value = 'Test value');
$this->assertTrue(isset($_SESSION[$key]), 'Session value should exist after set' . $suffix);
$this->assertEquals($value, $_SESSION[$key], 'Bad session set result' . $suffix);
// Update session value
Session::set($key, $value = 'Test value 2');
$this->assertEquals($value, $_SESSION[$key], 'Bad session update result' . $suffix);
// Delete session value
Session::remove($key);
$this->assertFalse(isset($_SESSION[$key]), 'Session value should be deleted' . $suffix);
// Load existing session
Session::set($key, $value);
Session::end();
$this->assertEquals($sid, Cookies::get('phwoolcon')->getValue(), 'Session cookie not set properly' . $suffix);
$this->assertFalse(isset($_SESSION), '$_SESSION should be ended' . $suffix);
$this->assertTrue(Session::start(), 'Failed to restart session' . $suffix);
$this->assertEquals($value, $_SESSION[$key], 'Bad session load result' . $suffix);
Session::end();
Config::set('session.default', 'native');
Session::register($this->di);
}
public function testSessionCRUDNative()
{
$this->realTestSessionCRUD('native');
}
public function testSessionCRUDRedis()
{
if (!extension_loaded('redis')) {
$this->markTestSkipped('The "redis" extension is not available.');
return;
}
$this->realTestSessionCRUD('redis');
}
public function testSessionCRUDMemcached()
{
if (!extension_loaded('memcached')) {
$this->markTestSkipped('The "memcached" extension is not available.');
return;
}
$this->realTestSessionCRUD('memcached');
}
public function testSessionFormData()
{
Config::set('session.default', 'native');
Session::register($this->di);
Session::start();
Session::rememberFormData($key = 'test-form', $value = ['k' => $v = 'v']);
$this->assertEquals($value, Session::getFormData($key), 'Session form data not set properly');
$this->assertEquals($v, Session::getFormData($key . '.k'), 'Unable to fetch sub element from form data');
$this->assertNull(Session::getFormData($badKey = 'bad-key'), 'Should get null for non-existing form data');
$this->assertEquals($v, Session::getFormData($badKey, $v), 'Unable to return default value');
Session::clearFormData($key);
$this->assertNull(Session::getFormData($key), 'Unable to clear session form data');
Session::end();
}
public function testSessionCsrfToken()
{
Config::set('session.default', 'native');
Session::register($this->di);
Session::start();
$this->assertNotEmpty($csrf = Session::generateCsrfToken(), 'Unable to generate CSRF token');
$this->assertEquals($csrf, Session::getCsrfToken(), 'Unable to check CSRF token');
Session::clear();
$this->assertNotEmpty($newCsrf = Session::getCsrfToken(), 'Unable to regenerate CSRF token');
$this->assertNotEquals($csrf, $newCsrf, 'Unable to regenerate unique CSRF token');
Session::end();
}
}
<?php
namespace Phwoolcon\Tests\Unit\View;
use Phalcon\Tag;
use Phwoolcon\Exception\WidgetException;
use Phwoolcon\Session;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\Tests\Helper\TestWidget;
use Phwoolcon\View\Widget;
class WidgetTest extends TestCase
{
public function testCsrfTokenField()
{
$widget = Widget::csrfTokenField();
$this->assertStringStartsWith('<input type="hidden" name="_token" value="', $widget);
$this->assertContains(Session::getCsrfToken(), $widget);
}
public function testLabel()
{
$widget = Widget::label(['for' => 'world'], 'Hello');
$this->assertEquals('<label for="world">Hello</label>', $widget);
}
public function testMultipleSelect()
{
// Check required field
$e = null;
try {
Widget::multipleChoose([]);
} catch (WidgetException $e) {
}
$this->assertInstanceOf(WidgetException::class, $e);
$this->assertContains('id', $e->getMessage());
$e = null;
try {
Widget::multipleChoose(['id' => 'hello']);
} catch (WidgetException $e) {
}
$this->assertInstanceOf(WidgetException::class, $e);
$this->assertContains('options', $e->getMessage());
// Test expanded multiple choose
$widget = Widget::multipleChoose([
'id' => 'hello',
'name' => 'data[hello]',
'options' => [
'hello' => 'world',
'foo' => 'bar',
],
]);
$startsWith = '<label for="hello_0"><input type="checkbox" id="hello_0" name="data[hello]" value="hello"';
$this->assertStringStartsWith($startsWith, $widget);
$this->assertContains('world</label>', $widget);
$contains = '<label for="hello_1"><input type="checkbox" id="hello_1" name="data[hello]" value="foo"';
$this->assertContains($contains, $widget);
$this->assertStringEndsWith('bar</label>', $widget);
// Test checked expanded multiple choose with label on left
$widget = Widget::multipleChoose([
'id' => 'hello',
'name' => 'data[hello]',
'value' => 'hello',
'labelOn' => 'left',
'options' => [
'hello' => 'world',
'foo' => 'bar',
],
]);
$this->assertStringStartsWith('<label for="hello_0">world', $widget);
$expected = 'input type="checkbox" id="hello_0" name="data[hello]" value="hello" checked="checked"';
$this->assertContains($expected, $widget);
$this->assertContains('<label for="hello_1">bar', $widget);
$this->assertContains('input type="checkbox" id="hello_1" name="data[hello]" value="foo"', $widget);
// Test multiple choose with select
$widget = Widget::multipleChoose([
'id' => 'hello',
'name' => 'data[hello]',
'expand' => false,
'options' => [
'hello' => 'world',
'foo' => 'bar',
],
]);
$this->assertContains('select id="hello" name="data[hello]"', $widget);
$this->assertContains('<option value="hello">world</option>', $widget);
$this->assertContains('<option value="foo">bar</option>', $widget);
// Test selected multiple choose with select
$widget = Widget::multipleChoose([
'id' => 'hello',
'name' => 'data[hello]',
'value' => ['hello', 'foo'],
'expand' => false,
'options' => [
'hello' => 'world',
'foo' => 'bar',
],
]);
$this->assertContains('select id="hello" name="data[hello]"', $widget);
$this->assertContains('<option selected="selected" value="hello">world</option>', $widget);
$this->assertContains('<option selected="selected" value="foo">bar</option>', $widget);
}
public function testSingleSelect()
{
// Check required field
$e = null;
try {
Widget::singleChoose([]);
} catch (WidgetException $e) {
}
$this->assertInstanceOf(WidgetException::class, $e);
$this->assertContains('id', $e->getMessage());
$e = null;
try {
Widget::singleChoose(['id' => 'hello']);
} catch (WidgetException $e) {
}
$this->assertInstanceOf(WidgetException::class, $e);
$this->assertContains('options', $e->getMessage());
// Test expanded single choose
$widget = Widget::singleChoose([
'id' => 'hello',
'name' => 'data[hello]',
'options' => [
'hello' => 'world',
'foo' => 'bar',
],
]);
$startsWith = '<label for="hello_0"><input type="radio" id="hello_0" name="data[hello]" value="hello"';
$this->assertStringStartsWith($startsWith, $widget);
$this->assertContains('world</label>', $widget);
$contains = '<label for="hello_1"><input type="radio" id="hello_1" name="data[hello]" value="foo"';
$this->assertContains($contains, $widget);
$this->assertStringEndsWith('bar</label>', $widget);
// Test checked expanded single choose with label on left
$widget = Widget::singleChoose([
'id' => 'hello',
'name' => 'data[hello]',
'value' => 'hello',
'labelOn' => 'left',
'options' => [
'hello' => 'world',
'foo' => 'bar',
],
]);
$this->assertStringStartsWith('<label for="hello_0">world', $widget);
$expected = 'input type="radio" id="hello_0" name="data[hello]" value="hello" checked="checked"';
$this->assertContains($expected, $widget);
$this->assertContains('<label for="hello_1">bar', $widget);
$this->assertContains('input type="radio" id="hello_1" name="data[hello]" value="foo"', $widget);
// Test single choose with select
$widget = Widget::singleChoose([
'id' => 'hello',
'name' => 'data[hello]',
'expand' => false,
'options' => [
'hello' => 'world',
'foo' => 'bar',
],
]);
$this->assertContains('select id="hello" name="data[hello]"', $widget);
$this->assertContains('<option value="hello">world</option>', $widget);
$this->assertContains('<option value="foo">bar</option>', $widget);
// Test selected single choose with select
$widget = Widget::singleChoose([
'id' => 'hello',
'name' => 'data[hello]',
'value' => 'hello',
'expand' => false,
'options' => [
'hello' => 'world',
'foo' => 'bar',
],
]);
$this->assertContains('select id="hello" name="data[hello]"', $widget);
$this->assertContains('<option selected="selected" value="hello">world</option>', $widget);
$this->assertContains('<option value="foo">bar</option>', $widget);
}
public function testUndefinedWidget()
{
$e = null;
try {
Widget::someWhat([]);
} catch (WidgetException $e) {
}
$this->assertInstanceOf(WidgetException::class, $e);
$this->assertContains('undefined', $e->getMessage(), '', true);
$this->assertContains('someWhat', $e->getMessage());
}
public function testDefine()
{
Widget::define('hello', function ($parameters) {
$innerHtml = isset($parameters['innerHtml']) ? $parameters['innerHtml'] : '';
unset($parameters['innerHtml']);
return Tag::tagHtml('hello', $parameters) . $innerHtml . Tag::tagHtmlClose('hello');
});
$widget = Widget::hello(['data-hello' => 'world']);
$this->assertEquals('<hello data-hello="world"></hello>', $widget);
}
public function testIdeGenerator()
{
Widget::define('hello', function (
array $parameters,
&$null = null,
$true = true,
$false = false,
$const = PHP_EOL,
\stdClass $class = null,
callable $callable = null
) {
$innerHtml = isset($parameters['innerHtml']) ? $parameters['innerHtml'] : '';
unset($parameters['innerHtml']);
return Tag::tagHtml('hello', $parameters) . $innerHtml . Tag::tagHtmlClose('hello');
});
$expected = ' public static function hello(array $parameters, &$null = null, ' .
'$true = true, $false = false, $const = PHP_EOL, stdClass $class = null, callable $callable = null) {}';
$this->assertEquals($expected, Widget::ideHelperGenerator());
Widget::define('hello', [new TestWidget(), 'hello']);
Widget::define('helloStatic', [TestWidget::class, 'helloStatic']);
$expected = <<<'METHODS'
public static function hello($p1) {
return (new Phwoolcon\Tests\Helper\TestWidget)->hello($p1);
}
public static function helloStatic($p1, array $p2 = array()) {
return Phwoolcon\Tests\Helper\TestWidget::helloStatic($p1, $p2);
}
METHODS;
$this->assertEquals($expected, Widget::ideHelperGenerator());
}
}
<?php
namespace Phwoolcon\Tests\Unit;
use Phwoolcon\Cache\Clearer;
use Phwoolcon\Tests\Helper\TestCase;
use Phwoolcon\View;
class ViewTest extends TestCase
{
public function setUp()
{
parent::setUp();
View::register($this->di);
}
public function testGetPhwoolconJsOptions()
{
$jsOptions = View::getPhwoolconJsOptions();
$this->assertArrayHasKey('baseUrl', $jsOptions, 'View::getPhwoolconJsOptions(): baseUrl not set');
$this->assertArrayHasKey('cookies', $jsOptions, 'View::getPhwoolconJsOptions(): cookies not set');
$this->assertArrayHasKey(
'domain',
fnGet($jsOptions, 'cookies'),
'View::getPhwoolconJsOptions(): cookies.domain not set'
);
$this->assertArrayHasKey(
'path',
fnGet($jsOptions, 'cookies'),
'View::getPhwoolconJsOptions(): cookies.path not set'
);
}
public function testMake()
{
View::noHeader(true);
View::noFooter(true);
$this->assertEquals("TEST MAKE", View::make('test', 'make'), 'Bad View::make() content');
}
public function testRenderWithTwoParameters()
{
View::noHeader(true);
View::noFooter(true);
$this->assertEquals("I got the world", View::make('test/two-parameters', [
'param' => 'the world',
]), 'Bad View::render() content with 2 parameters');
}
public function testPhpTemplateInclude()
{
$this->assertEquals("TEST MAKE", View::make('test', 'include'), 'Bad View Php engine include content');
}
public function testPageVariables()
{
/* @var View $view */
$view = $this->di->getShared('view');
$view->setParams($params = [
'page_title' => 'Test Title',
'page_keywords' => 'Test Keyword1,Test Keyword2',
'page_description' => 'Test Description',
]);
$this->assertEquals($params['page_title'], View::getPageTitle());
$this->assertEquals('zh-CN', View::getPageLanguage());
$this->assertEquals($params['page_keywords'], View::getPageKeywords());
$this->assertEquals($params['page_description'], View::getPageDescription());
}
public function testAssets()
{
/* @var View $view */
$view = $this->di->getShared('view');
$view->isAdmin(false);
$view->cache(true);
Clearer::clear(Clearer::TYPE_ASSETS);
$this->assertStringStartsWith('<script type="text/javascript" ', View::generateHeadJs());
// Test assets cache
$this->assertStringStartsWith('<script type="text/javascript" ', View::generateHeadJs());
$this->assertStringStartsWith('<link rel="stylesheet" type="text/css" ', View::generateHeadCss());
$this->assertStringStartsWith('<link rel="stylesheet" type="text/css" ', View::generateIeHack());
$this->assertStringStartsWith('<script type="text/javascript" ', View::generateBodyJs());
$this->assertStringStartsWith('<script type="text/javascript" ', View::generateIeHackBodyJs());
$this->assertStringStartsWith('<script type="text/javascript" ', View::assets('non-existing-remote-js'));
}
}
#!/usr/bin/env bash
cd "$(dirname "$( dirname "${BASH_SOURCE[0]}" )")"
vendor/bin/phpcs --report-full=tests/root/storage/phpcs.txt
#!/usr/bin/env bash
cd "$(dirname "$( dirname "${BASH_SOURCE[0]}" )")"
phpmetrics --report-html tests/root/storage/phpmetrics.html
#!/usr/bin/env bash
cd "$(dirname "$( dirname "${BASH_SOURCE[0]}" )")"
rm -rf /tmp/phwoolcon-test/
> tests/root/storage/logs/phwoolcon.log
> tests/root/storage/logs/service.log
vendor/bin/phpunit --coverage-html tests/root/storage/coverage "$@"
<?php
return [
'overridden' => true,
];
<?php
use Phwoolcon\Cli\Command\ClearCacheCommand;
use Phwoolcon\Cli\Command\Migrate;
use Phwoolcon\Cli\Command\MigrateCreate;
use Phwoolcon\Cli\Command\MigrateList;
use Phwoolcon\Cli\Command\MigrateRevert;
use Phwoolcon\Cli\Command\QueueConsumeCommand;
use Phwoolcon\Tests\Helper\Cli\TestCommand;
return [
'test-command' => TestCommand::class,
'clear:cache' => ClearCacheCommand::class,
'migrate' => Migrate::class,
'migrate:create' => MigrateCreate::class,
'migrate:revert' => MigrateRevert::class,
'migrate:list' => MigrateList::class,
'queue:consume' => QueueConsumeCommand::class,
];
<?php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'dbname' => 'phwoolcon_test',
],
],
];
<?php
return [
'enabled' => true,
'smtp_host' => 'localhost',
'smtp_port' => 2500,
];
<?php
return [
'queues' => [
'default_queue' => [
'options' => [
'default' => 'phwoolcon-test',
],
],
],
];
<?php
return [
'worker_num' => 1,
'daemonize' => 0,
'max_request' => 10,
'backlog' => 8192,
'run_dir' => '/tmp/phwoolcon-test/',
'linux_init_script' => '/etc/init.d/phwoolcon-test',
'debug' => true,
];
<?php
return [
'assets' => [
'head-css' => [
'css/test.css',
],
'head-js' => [
'js/test.js',
],
'body-js' => [
'js/test.js',
],
'ie-hack-css' => [
],
'ie-hack-js' => [
],
'ie-hack-body-js' => [
],
'non-existing-remote-js' => [
'http://127.0.0.1:8888/non-existing.js',
],
],
'admin' => [
'title_suffix' => 'Admin',
'theme' => 'default',
'layout' => 'default',
'assets' => [
'head-css' => [
'test.css',
],
'head-js' => [
'test.js',
],
'body-js' => [
'test-body.js',
],
'ie-hack-css' => [
'test-ie-hack.css',
],
'ie-hack-js' => [
'test-ie-hack.js',
],
'ie-hack-body-js' => [
'test-ie-hack-body.js',
],
],
],
'options' => [
'assets_options' => [
'cdn_prefix' => 'https://cdn.example.com',
],
],
];
<?php
return [
'Test' => 'Test 1',
];
<?php
return [
'Test' => 'Test',
];
<?php
return [
'test_error' => 'Test error message',
'test_param' => 'Test error message with %param% and %another_param%',
'1234' => 'Test numeric error code',
'2345_with_annotation' => 'Test numeric error code with annotation',
];
<?php
return [
'Test' => '测试1',
];
<?php
return [
'Test' => '测试',
];
<?php
return [
'test_error' => 'Test error message',
'test_param' => 'Test error message with %param% and %another_param%',
'1234' => 'Test numeric error code',
'2345_with_annotation' => 'Test numeric error code with annotation',
];
<?php
/* @var Phwoolcon\Router $this */
use Phwoolcon\Tests\Helper\Filter\AlwaysException as AlwaysExceptionFilter;
use Phwoolcon\Tests\Helper\Filter\AlwaysFail as AlwaysFailFilter;
$this->prefix('/prefix', [
'GET' => [
'test-route' => 'Phwoolcon\Tests\Helper\TestController::getTestPrefixedRoute',
'test-callable-route' => [Phwoolcon\Tests\Helper\TestController::class, 'getTestPrefixedRoute'],
],
])->prefix('/api', [
'GET' => [
'/:params' => 'Phwoolcon\Tests\Helper\TestApiController::missingMethod',
'test-route' => 'Phwoolcon\Tests\Helper\TestApiController::getTestRoute',
'test-json-api-data' => 'Phwoolcon\Tests\Helper\TestApiController::getJsonApiData',
'test-json-api-error' => 'Phwoolcon\Tests\Helper\TestApiController::getJsonApiError',
'test-json-api-meta' => 'Phwoolcon\Tests\Helper\TestApiController::getJsonApiMeta',
],
], MultiFilter::instance()
->add(DisableSessionFilter::instance())
->add(DisableCsrfFilter::instance()))
->prefix('/admin', [
'GET' => [
'test-route' => 'Phwoolcon\Tests\Helper\TestAdminController::getTestRoute',
],
]);
return [
'GET' => [
'test/{what}/regex-route' => [
'controller' => function () {
/* @var Router $this */
return var_export($this->getParams(), 1);
},
'params' => 1, // Test params mapping
'module' => 'foo', // Just to cover codes,
'namespace' => 'bar', // phwoolcon don't use module and namespace
],
'test-closure-route' => function () {
return 'Test Closure Route Content';
},
'test-controller-route' => 'Phwoolcon\Tests\Helper\TestController::getTestRoute',
'test-controller-input' => 'Phwoolcon\Tests\Helper\TestController::getTestInput',
'test-exception-filter-route' => [
'controller' => function () {
return 'Wont be executed';
},
'filter' => AlwaysExceptionFilter::instance(),
],
],
'POST' => [
'test-filtered-route' => [
'controller' => 'Phwoolcon\Tests\Helper\TestController',
'action' => 'getTestRoute',
'filter' => MultiFilter::instance()
->add(DisableSessionFilter::instance())
->add(DisableCsrfFilter::instance()),
],
'test-failed-filter-route' => [
'controller' => function () {
return 'Wont be executed';
},
'filter' => MultiFilter::instance()
->add(AlwaysFailFilter::instance())
->add(DisableSessionFilter::instance())
->add(DisableCsrfFilter::instance()),
],
'test-csrf-check' => function () {
return 'Wont be executed';
},
],
];
<?php
/* @var Phwoolcon\View\Engine\Php $this */
?>
<?php $this->include('test/make') ?>
<?php
$param = View::getParam('param');
echo "I got {$param}";
<?php
/* @var Phwoolcon\View\Engine\Php $this */
?>
<?= $this->getContent(); ?>
[Session]
session.use_cookies = 0
session.cache_limiter =
[Redis]
extension="redis.so"
[Memcached]
extension="memcached.so"
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!