first commit

This commit is contained in:
2026-01-25 18:18:09 +08:00
commit 509312e604
8136 changed files with 2349298 additions and 0 deletions

View File

29
LICENSE.md Normal file
View File

@ -0,0 +1,29 @@
Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Yii Software LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

233
README.md Normal file
View File

@ -0,0 +1,233 @@
<p align="center">
<a href="https://github.com/yiisoft" target="_blank">
<img src="https://avatars0.githubusercontent.com/u/993323" height="100px">
</a>
<h1 align="center">Yii 2 Basic Project Template</h1>
<br>
</p>
Yii 2 Basic Project Template is a skeleton [Yii 2](https://www.yiiframework.com/) application best for
rapidly creating small projects.
The template contains the basic features including user login/logout and a contact page.
It includes all commonly used configurations that would allow you to focus on adding new
features to your application.
[![Latest Stable Version](https://img.shields.io/packagist/v/yiisoft/yii2-app-basic.svg)](https://packagist.org/packages/yiisoft/yii2-app-basic)
[![Total Downloads](https://img.shields.io/packagist/dt/yiisoft/yii2-app-basic.svg)](https://packagist.org/packages/yiisoft/yii2-app-basic)
[![build](https://github.com/yiisoft/yii2-app-basic/workflows/build/badge.svg)](https://github.com/yiisoft/yii2-app-basic/actions?query=workflow%3Abuild)
DIRECTORY STRUCTURE
-------------------
assets/ contains assets definition
commands/ contains console commands (controllers)
config/ contains application configurations
controllers/ contains Web controller classes
mail/ contains view files for e-mails
models/ contains model classes
runtime/ contains files generated during runtime
tests/ contains various tests for the basic application
vendor/ contains dependent 3rd-party packages
views/ contains view files for the Web application
web/ contains the entry script and Web resources
REQUIREMENTS
------------
The minimum requirement by this project template that your Web server supports PHP 7.4.
INSTALLATION
------------
### Install via Composer
If you do not have [Composer](https://getcomposer.org/), you may install it by following the instructions
at [getcomposer.org](https://getcomposer.org/doc/00-intro.md#installation-nix).
You can then install this project template using the following command:
~~~
composer create-project --prefer-dist yiisoft/yii2-app-basic basic
~~~
Now you should be able to access the application through the following URL, assuming `basic` is the directory
directly under the Web root.
~~~
http://localhost/basic/web/
~~~
### Install from an Archive File
Extract the archive file downloaded from [yiiframework.com](https://www.yiiframework.com/download/) to
a directory named `basic` that is directly under the Web root.
Set cookie validation key in `config/web.php` file to some random secret string:
```php
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '<secret random string goes here>',
],
```
You can then access the application through the following URL:
~~~
http://localhost/basic/web/
~~~
### Install with Docker
Update your vendor packages
docker-compose run --rm php composer update --prefer-dist
Run the installation triggers (creating cookie validation code)
docker-compose run --rm php composer install
Start the container
docker-compose up -d
You can then access the application through the following URL:
http://127.0.0.1:8000
**NOTES:**
- Minimum required Docker engine version `17.04` for development (see [Performance tuning for volume mounts](https://docs.docker.com/docker-for-mac/osxfs-caching/))
- The default configuration uses a host-volume in your home directory `.docker-composer` for composer caches
CONFIGURATION
-------------
### Database
Edit the file `config/db.php` with real data, for example:
```php
return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2basic',
'username' => 'root',
'password' => '1234',
'charset' => 'utf8',
];
```
**NOTES:**
- Yii won't create the database for you, this has to be done manually before you can access it.
- Check and edit the other files in the `config/` directory to customize your application as required.
- Refer to the README in the `tests` directory for information specific to basic application tests.
TESTING
-------
Tests are located in `tests` directory. They are developed with [Codeception PHP Testing Framework](https://codeception.com/).
By default, there are 3 test suites:
- `unit`
- `functional`
- `acceptance`
Tests can be executed by running
```
vendor/bin/codecept run
```
The command above will execute unit and functional tests. Unit tests are testing the system components, while functional
tests are for testing user interaction. Acceptance tests are disabled by default as they require additional setup since
they perform testing in real browser.
### Running acceptance tests
To execute acceptance tests do the following:
1. Rename `tests/acceptance.suite.yml.example` to `tests/acceptance.suite.yml` to enable suite configuration
2. Replace `codeception/base` package in `composer.json` with `codeception/codeception` to install full-featured
version of Codeception
3. Update dependencies with Composer
```
composer update
```
4. Download [Selenium Server](https://www.seleniumhq.org/download/) and launch it:
```
java -jar ~/selenium-server-standalone-x.xx.x.jar
```
In case of using Selenium Server 3.0 with Firefox browser since v48 or Google Chrome since v53 you must download [GeckoDriver](https://github.com/mozilla/geckodriver/releases) or [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) and launch Selenium with it:
```
# for Firefox
java -jar -Dwebdriver.gecko.driver=~/geckodriver ~/selenium-server-standalone-3.xx.x.jar
# for Google Chrome
java -jar -Dwebdriver.chrome.driver=~/chromedriver ~/selenium-server-standalone-3.xx.x.jar
```
As an alternative way you can use already configured Docker container with older versions of Selenium and Firefox:
```
docker run --net=host selenium/standalone-firefox:2.53.0
```
5. (Optional) Create `yii2basic_test` database and update it by applying migrations if you have them.
```
tests/bin/yii migrate
```
The database configuration can be found at `config/test_db.php`.
6. Start web server:
```
tests/bin/yii serve
```
7. Now you can run all available tests
```
# run all available tests
vendor/bin/codecept run
# run acceptance tests
vendor/bin/codecept run acceptance
# run only unit and functional tests
vendor/bin/codecept run unit,functional
```
### Code coverage support
By default, code coverage is disabled in `codeception.yml` configuration file, you should uncomment needed rows to be able
to collect code coverage. You can run your tests and collect coverage with the following command:
```
#collect coverage for all tests
vendor/bin/codecept run --coverage --coverage-html --coverage-xml
#collect coverage only for unit tests
vendor/bin/codecept run unit --coverage --coverage-html --coverage-xml
#collect coverage for unit and functional tests
vendor/bin/codecept run functional,unit --coverage --coverage-html --coverage-xml
```
You can see code coverage output under the `tests/_output` directory.

163
Serializers/Serializer.php Normal file
View File

@ -0,0 +1,163 @@
<?php
namespace app\serializers;
class Serializer
{
/* ========= 规则存储 ========= */
protected array $handlerTrie = [];
protected array $removeTrie = [];
protected array $appendRules = [];
public function __construct(
protected array|null $source = null
) {}
/* ========= API ========= */
public function handle(string $path, callable $fn): static
{
$this->insertTrie($this->handlerTrie, explode('.', $path), $fn);
return $this;
}
public function remove(string $path): static
{
$this->insertTrie($this->removeTrie, explode('.', $path), true);
return $this;
}
/**
* 节点级追加path 指向节点)
*/
public function append(string $path, callable $fn): static
{
$this->appendRules[] = [
'path' => explode('.', $path),
'fn' => $fn,
];
return $this;
}
/* ========= 执行 ========= */
public function serialize(): array
{
$data = $this->source ?? [];
// Phase 1字段级Trie
$this->walkTrie($data, $this->handlerTrie, $this->removeTrie);
// Phase 2节点级append
$this->applyAppend($data);
return $data;
}
/* ========= Phase 1 ========= */
protected function walkTrie(
&$node,
array $handlerNode,
array $removeNode
): void {
if (!is_array($node)) return;
foreach ($node as $key => &$value) {
// list不消费 trie
if (is_int($key)) {
$this->walkTrie($value, $handlerNode, $removeNode);
continue;
}
$nextHandler = $handlerNode[$key] ?? [];
$nextRemove = $removeNode[$key] ?? [];
// 删除优先
if (isset($nextRemove['_end'])) {
unset($node[$key]);
continue;
}
// 修改字段
if (isset($nextHandler['_fn'])) {
$value = ($nextHandler['_fn'])($value);
}
if (is_array($value)) {
$this->walkTrie($value, $nextHandler, $nextRemove);
}
}
}
/* ========= Phase 2 ========= */
protected function applyAppend(array &$data): void
{
foreach ($this->appendRules as $rule) {
$this->walkAppend($data, $rule['path'], $rule['fn']);
}
}
protected function walkAppend(
&$node,
array $path,
callable $fn
): void {
if (!is_array($node)) return;
// 命中节点
if (empty($path)) {
if (!$this->isList($node)) {
$fn($node);
}
return;
}
$key = array_shift($path);
if ($this->isList($node)) {
foreach ($node as &$item) {
$this->walkAppend($item, array_merge([$key], $path), $fn);
}
return;
}
if (isset($node[$key])) {
$this->walkAppend($node[$key], $path, $fn);
}
}
/* ========= Trie ========= */
protected function insertTrie(array &$trie, array $path, $value): void
{
$node = &$trie;
foreach ($path as $segment) {
if (!isset($node[$segment])) {
$node[$segment] = [];
}
$node = &$node[$segment];
}
if (is_callable($value)) {
$node['_fn'] = $value;
} else {
$node['_end'] = true;
}
}
protected function isList(array $arr): bool
{
return array_keys($arr) === range(0, count($arr) - 1);
}
}

92
Vagrantfile vendored Normal file
View File

@ -0,0 +1,92 @@
require 'yaml'
require 'fileutils'
required_plugins_installed = nil
required_plugins = %w( vagrant-hostmanager vagrant-vbguest )
required_plugins.each do |plugin|
unless Vagrant.has_plugin? plugin
system "vagrant plugin install #{plugin}"
required_plugins_installed = true
end
end
# IF plugin[s] was just installed - restart required
if required_plugins_installed
# Get CLI command[s] and call again
system 'vagrant' + ARGV.to_s.gsub(/\[\"|\", \"|\"\]/, ' ')
exit
end
domains = {
app: 'yii2basic.test'
}
vagrantfile_dir_path = File.dirname(__FILE__)
config = {
local: vagrantfile_dir_path + '/vagrant/config/vagrant-local.yml',
example: vagrantfile_dir_path + '/vagrant/config/vagrant-local.example.yml'
}
# copy config from example if local config not exists
FileUtils.cp config[:example], config[:local] unless File.exist?(config[:local])
# read config
options = YAML.load_file config[:local]
# check github token
if options['github_token'].nil? || options['github_token'].to_s.length != 40
puts "You must place REAL GitHub token into configuration:\n/yii2-app-basic/vagrant/config/vagrant-local.yml"
exit
end
# vagrant configurate
Vagrant.configure(2) do |config|
# select the box
config.vm.box = 'bento/ubuntu-18.04'
# should we ask about box updates?
config.vm.box_check_update = options['box_check_update']
config.vm.provider 'virtualbox' do |vb|
# machine cpus count
vb.cpus = options['cpus']
# machine memory size
vb.memory = options['memory']
# machine name (for VirtualBox UI)
vb.name = options['machine_name']
end
# machine name (for vagrant console)
config.vm.define options['machine_name']
# machine name (for guest machine console)
config.vm.hostname = options['machine_name']
# network settings
config.vm.network 'private_network', ip: options['ip']
# sync: folder 'yii2-app-advanced' (host machine) -> folder '/app' (guest machine)
config.vm.synced_folder './', '/app', owner: 'vagrant', group: 'vagrant'
# disable folder '/vagrant' (guest machine)
config.vm.synced_folder '.', '/vagrant', disabled: true
# hosts settings (host machine)
config.vm.provision :hostmanager
config.hostmanager.enabled = true
config.hostmanager.manage_host = true
config.hostmanager.ignore_private_ip = false
config.hostmanager.include_offline = true
config.hostmanager.aliases = domains.values
# quick fix for failed guest additions installations
# config.vbguest.auto_update = false
# provisioners
config.vm.provision 'shell', path: './vagrant/provision/once-as-root.sh', args: [options['timezone'], options['ip']]
config.vm.provision 'shell', path: './vagrant/provision/once-as-vagrant.sh', args: [options['github_token']], privileged: false
config.vm.provision 'shell', path: './vagrant/provision/always-as-root.sh', run: 'always'
# post-install message (vagrant console)
config.vm.post_up_message = "App URL: http://#{domains[:app]}"
end

35
assets/AdminAsset.php Normal file
View File

@ -0,0 +1,35 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace app\assets;
use yii\web\AssetBundle;
/**
* Main application asset bundle.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class AdminAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
// 'css/site.css',
'css/admin.css',
'css/layui.css',
'css/layer.css',
'font/iconfont.woff2'
];
public $js = [
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap5\BootstrapAsset'
];
}

36
assets/AppAsset.php Normal file
View File

@ -0,0 +1,36 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace app\assets;
use yii\web\AssetBundle;
/**
* Main application asset bundle.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
// 'css/site.css',
'css/layui.css',
'css/admin.css',
'css/layer.css',
'font/iconfont.woff2'
];
public $js = [
// 'js/jquery.js',
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap5\BootstrapAsset'
];
}

823
brand.txt Normal file
View File

@ -0,0 +1,823 @@
[{
"id": 6977,
"name": "\u65b0\u4e16\u5bb6\u65cf"
}, {
"id": 22569,
"name": "\u548c\u8a00"
}, {
"id": 86493,
"name": "\u4e91\u601d\u6728\u60f3"
}, {
"id": 86539,
"name": "\u7ec7\u91cc"
}, {
"id": 87583,
"name": "\u7b80\u598d"
}, {
"id": 89235,
"name": "\u5dfd\u5f69"
}, {
"id": 94111,
"name": "\u6717\u5764"
}, {
"id": 96581,
"name": "\u4e09\u6dfc"
}, {
"id": 98457,
"name": "\u5e84\u5bb9"
}, {
"id": 98555,
"name": "\u77f3\u72ee"
}, {
"id": 98609,
"name": "\u8513\u697c\u862d"
}, {
"id": 135437,
"name": "\u5409\u7965\u658b"
}, {
"id": 176185,
"name": "-by RYOJI OBATA"
}, {
"id": 179409,
"name": "\u4e39\u5c3c\u65af"
}, {
"id": 179587,
"name": "\u5a75\u4e4b\u4e91"
}, {
"id": 181183,
"name": "(A)crypsis"
}, {
"id": 188961,
"name": "\u690d\u6728"
}, {
"id": 194536,
"name": "\u4e0a\u624b\u82b1\u7530"
}, {
"id": 198855,
"name": ".ru"
}, {
"id": 194598,
"name": "\u4e91\u51e4\u601d\u5112"
}, {
"id": 199655,
"name": "#SEN"
}, {
"id": 207855,
"name": "\u00c9tudes"
}, {
"id": 206285,
"name": "(X)S.M.L"
}, {
"id": 202839,
"name": "#TodaBelezaPodeSer"
}, {
"id": 204531,
"name": "\u82b1\u6728\u6df1"
}, {
"id": 209372,
"name": "\u201c\u670d\u201d\u5938\u00b7\u201c\u9970\u201d\u653e"
}, {
"id": 200059,
"name": "\u7396\u5586\u5c0f\u7ae5"
}, {
"id": 202583,
"name": "#CraftIsCool"
}, {
"id": 209339,
"name": "\u4e0a\u624b\uff06HELY\u00b7\u9648\u5b87"
}, {
"id": 209623,
"name": "\u041d\u043e\u0432\u044b\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438"
}, {
"id": 209704,
"name": "\u0428\u0418\u0422\u042c PROSTO"
}, {
"id": 215967,
"name": "(di)vision"
}, {
"id": 219118,
"name": "\u51dd\u5948\u6620"
}, {
"id": 210111,
"name": "\u0420\u0443\u0441\u0441\u043a\u0438\u0439 \u0441\u0438\u043b\u0443\u044d\u0442"
}, {
"id": 211798,
"name": "\u041e\u043b\u043e\u0432\u043e"
}, {
"id": 211878,
"name": "\u042d\u0444\u0435\u043c\u0435\u0440\u0438\u0434\u0430 DK"
}, {
"id": 212677,
"name": "\u6734\u4e0e\u7d20"
}, {
"id": 214536,
"name": "\u0421\u0435\u043a\u0446\u0438\u044f"
}, {
"id": 214792,
"name": "\u6fb3\u95e8\u65f6\u5c1a\u6c47\u6f14"
}, {
"id": 217968,
"name": "\u84dd\u9014"
}, {
"id": 218285,
"name": "\u5fb7\u724c&\u745e\u724c"
}, {
"id": 218902,
"name": "\u534e\u88f3\u4e5d\u5dde"
}, {
"id": 218998,
"name": "\u548cROBE"
}, {
"id": 219929,
"name": "\u8d62\u667a\u5c1a"
}, {
"id": 210393,
"name": "+81 BRANCA"
}, {
"id": 211080,
"name": "\u0412\u0418\u041d\u041d\u0418"
}, {
"id": 211898,
"name": "\u0415\u0441\u0435\u043d\u0438\u044f"
}, {
"id": 211899,
"name": "\u041a\u0443\u0437\u043d\u0435\u0446\u043e\u0432\u0430 \u0438 \u0411\u043e\u043d\u0438\u0444\u0430\u0442\u044c\u0435\u0432\u0430"
}, {
"id": 211900,
"name": "\u0421\u0442\u0438\u043b\u044f\u0436\u043a\u0438"
}, {
"id": 212394,
"name": "\u4e00\u4e0a\u5341"
}, {
"id": 212676,
"name": "\u5e74\u8863"
}, {
"id": 214556,
"name": "\u0421\u0427\u0410\u0421\u0422\u042c\u0415"
}, {
"id": 214591,
"name": "+7 HOURS BY IVAN ASEN 22"
}, {
"id": 214752,
"name": "\u9488\u7ec7\u529b\u91cf"
}, {
"id": 214755,
"name": "\u7ae0\u6d77\u71d5"
}, {
"id": 214757,
"name": "\u6750\u8d28\u5b9e\u9a8c"
}, {
"id": 214758,
"name": "\u539f\u6c11\u6587\u5316"
}, {
"id": 214774,
"name": "\u6d6a\u5de2"
}, {
"id": 214781,
"name": "\u5b9e\u9a8c\u7535\u5b50"
}, {
"id": 214782,
"name": "\u5eb6\u6c11\u751f\u6d3b"
}, {
"id": 214783,
"name": "\u6c38\u7eed\u521b\u65b0"
}, {
"id": 214785,
"name": "\u8bbe\u8ba1\u5e08\u8054\u5408\u79c0"
}, {
"id": 214793,
"name": "\u5b66\u751f\u8bbe\u8ba1\u5e08\u4e13\u573a"
}, {
"id": 214795,
"name": "\u539f\u521b\u8bbe\u8ba1\u5927\u8d5b\u603b\u51b3\u8d5b"
}, {
"id": 214798,
"name": "\u65f6\u5c1a\u8bbe\u8ba1\u5b66\u9662\u5b75\u5316\u8bbe\u8ba1\u54c1\u724c\u8054\u6f14"
}, {
"id": 215031,
"name": "\u0421\u043ek\u043en R\u0441hbin"
}, {
"id": 217179,
"name": "\u00c0LEA"
}, {
"id": 218203,
"name": "\u69ff\u7eed\u65d7\u888dx\u4e94\u5343\u9ad8\u8857"
}, {
"id": 218282,
"name": "\u5143\u9882 \u00b7 \u4e1c\u65b9"
}, {
"id": 218286,
"name": "\u84dd\u9edb\u4e39\u5c3c"
}, {
"id": 218289,
"name": "\u6d77\u6d3e"
}, {
"id": 219397,
"name": "\u5341\u4e8c\u9762\u4f53x\u805a\u9053"
}, {
"id": 219700,
"name": "\u0421hereshnivska x Tereza Barabash"
}, {
"id": 219977,
"name": "\u94ed\u8863\u574a"
}, {
"id": 228189,
"name": "\u53e4\u963f\u65b0"
}, {
"id": 221776,
"name": "\u8d70\u5fc3\u00b7\u83a8\u7ef8"
}, {
"id": 229797,
"name": "\u5929\u73ba"
}, {
"id": 221544,
"name": "\u767d\u9e7f\u8bed \u00b7 \u8d75\u6653\u59b9"
}, {
"id": 221851,
"name": "\u7280\u6d41"
}, {
"id": 224960,
"name": "\u9526\u88f3\u7389\u8863"
}, {
"id": 220131,
"name": "\u6652\u8c37\u573a"
}, {
"id": 224385,
"name": "\u91d1\u666f\u6021"
}, {
"id": 220084,
"name": "\u94cc\u91c7\u7eba\u7ec7 X \u610f\u7136\u513f\u670d\u88c5\u8bbe\u8ba1"
}, {
"id": 221896,
"name": "\u68df\u6881 RUNWAY x JUNWEILIN"
}, {
"id": 221909,
"name": "\u6167\u971e"
}, {
"id": 221914,
"name": "\u767d\u9a6cX\u4e2d\u6e2f"
}, {
"id": 221932,
"name": "\u7ca4\u6e2f\u6fb3\u54c1\u724c"
}, {
"id": 221934,
"name": "\u82b1\u8a93"
}, {
"id": 221935,
"name": "\u4e91\u4e1d\u5c1a"
}, {
"id": 221994,
"name": "\u5b8f\u6770\u96c6\u56e2"
}, {
"id": 224178,
"name": "\u79be\u96c0"
}, {
"id": 225586,
"name": "\u5343\u8da3\u4f1a"
}, {
"id": 225889,
"name": "\u5341\u4e8c\u9762\u4f53"
}, {
"id": 226836,
"name": "\u6cca\u96fe"
}, {
"id": 220081,
"name": "\u73ba\u94ed\u8bbe\u8ba1"
}, {
"id": 220106,
"name": "\u534e\u6d77\u8fbe\u8054\u5408\u79c0"
}, {
"id": 221661,
"name": "\u4e2d\u56fd\u674e\u5b81"
}, {
"id": 221688,
"name": "\u559c\u7ed2\u5154"
}, {
"id": 221735,
"name": "\u5706.\u5f5d"
}, {
"id": 221778,
"name": "\u805a\u9053 X \u5996\u5ba2"
}, {
"id": 221820,
"name": "\u601d\u672c\u5802\u00b7\u7f02\u4e1d"
}, {
"id": 221891,
"name": "\u805a\u9053 X LANG COUTURE"
}, {
"id": 221892,
"name": "\u68df\u6881 RUNWAY x DANSHAN"
}, {
"id": 221893,
"name": "\u68df\u6881 RUNWAY x CONCISE"
}, {
"id": 221907,
"name": "\u4e2d\u5c71\u88c5\u6587\u5316\u9986"
}, {
"id": 221908,
"name": "\u7384\u61ac\u9f99"
}, {
"id": 221910,
"name": "\u7814\u7ee3\u5de5\u623f"
}, {
"id": 221911,
"name": "\u4e7e\u946b\u94bb\u9970"
}, {
"id": 221966,
"name": "\u6797\u829d"
}, {
"id": 221968,
"name": "\u5cad\u5357\u8863\u88f3"
}, {
"id": 221970,
"name": "\u4e2d\u56fd\u6797\u829d X \u5218\u4eae"
}, {
"id": 221998,
"name": "\u9999\u683c\u96c5\u4e3d"
}, {
"id": 222002,
"name": "\u81ea\u7136\u800c\u3010\u71c3\u3011"
}, {
"id": 224012,
"name": "\u82b1\u7530\u5f69"
}, {
"id": 224092,
"name": "\u56fd\u8299"
}, {
"id": 224096,
"name": "\u83c1\u754c"
}, {
"id": 224179,
"name": "\u754c\u65d7"
}, {
"id": 224180,
"name": "\u6590\u674b"
}, {
"id": 224181,
"name": "\u4e56\u4e56\u718a"
}, {
"id": 224182,
"name": "\u9f99\u4e91\u4f20"
}, {
"id": 224184,
"name": "\u66e6\u5149"
}, {
"id": 224185,
"name": "\u8fe6\u7136"
}, {
"id": 224187,
"name": "\u5c3c\u53ef\u8da3\u73a9"
}, {
"id": 224190,
"name": "\u5410\u706b\u7f57\u7d22\u739b\u7f07"
}, {
"id": 224191,
"name": "\u9526\u7ee3\u4e2d\u534e"
}, {
"id": 224383,
"name": "\u7eff\u8272\u5e74\u534e"
}, {
"id": 224957,
"name": "\u794e\u8bd7\u742a"
}, {
"id": 225594,
"name": "\u5927\u65b9TAIFAR"
}, {
"id": 225887,
"name": "\u771f\u6797"
}, {
"id": 227121,
"name": "\u5e7c\u60a0"
}, {
"id": 228185,
"name": "\u5220\u96642"
}, {
"id": 228596,
"name": "\u805a\u9053x Always Lang"
}, {
"id": 228635,
"name": "\u4f70\u4f26\u4e16\u5bb6"
}, {
"id": 228636,
"name": "\u6a31\u59ff\u5a1c"
}, {
"id": 228641,
"name": "\u4e16\u6cf0\u667a\u9020"
}, {
"id": 228642,
"name": "\u534e\u7f8e"
}, {
"id": 228688,
"name": "\u7231\u9edb\u00b7\u7231\u7f8e"
}, {
"id": 236523,
"name": "\u5927\u8fde\u65f6\u88c5\u5468"
}, {
"id": 236969,
"name": "\u5e7f\u4e1c\u65f6\u88c5\u5468"
}, {
"id": 230127,
"name": "\u4e0a\u4e45\u6977"
}, {
"id": 230089,
"name": "\u8776\u5f71\u87f2\u87f2"
}, {
"id": 233005,
"name": "\u9f8d\u77f3\u5370\u8ff9"
}, {
"id": 234566,
"name": "\u5e7f\u4e1c\u975e\u9057\u670d\u88c5\u670d\u9970\u4f18\u79c0\u6848\u4f8b\u4f5c\u54c1"
}, {
"id": 239413,
"name": "\u8d70\u5fc3"
}, {
"id": 230124,
"name": "\u4f57\u5bc2"
}, {
"id": 230174,
"name": "\u9f8d\u77f3\u5370\u8ff9&LS"
}, {
"id": 233007,
"name": "\u80a9\u4e0a\u4e91"
}, {
"id": 232752,
"name": "#whysocerealz!"
}, {
"id": 237415,
"name": "\u00c6MONA"
}, {
"id": 230163,
"name": "\u7261\u4e39\u4ead"
}, {
"id": 230217,
"name": "\u68a6\u5e7b\u897f\u6e38\u00d7\u4e09\u5bf8\u76db\u4eac"
}, {
"id": 232856,
"name": "\"\u4e1d\u97f5\u4e1c\u65b9\"\u76db\u6cfd\u65f6\u5c1a"
}, {
"id": 232923,
"name": "\u9996\u5c4a\u5168\u56fd\u5c11\u5e74\u513f\u7ae5\u827a\u672f\u65f6\u5c1a\u98ce\u91c7\u5927\u4f1a"
}, {
"id": 232939,
"name": "\u5173\u96ce"
}, {
"id": 233400,
"name": "\u9192\u72ee\u549a\u549a\u9535"
}, {
"id": 234247,
"name": "\"ZETTAKIT CUP\"1st China Metaverse Fashion Design Competition\/\u6cfd\u5854\u4e91\u676f\u2022\u7b2c\u4e00\u5c4a\u4e2d\u56fd\u5143\u5b87\u5b99\u670d\u88c5\u8bbe\u8ba1\u5927\u8d5b"
}, {
"id": 234561,
"name": "_J.L-A.L_"
}, {
"id": 236544,
"name": "\u5409\u7965\u8863\u5bb6\u00b7\u9093\u6625\u65ed"
}, {
"id": 239012,
"name": "\u0422vid & Ko\/\u0422\u0432\u0438\u0434 & \u041a\u043e"
}, {
"id": 239319,
"name": "\u94ed\u6708\u670d\u9970"
}, {
"id": 230122,
"name": "\u745e\u5c14"
}, {
"id": 230123,
"name": "\u76ae\u57ce\u4e25\u9009"
}, {
"id": 231012,
"name": "\u8d8a\u7ea6\u65f6\u5c1a"
}, {
"id": 232957,
"name": "\u840c\u52a8\u4e00\u5927\u624b\u62c9\u5c0f\u624b\u300a\u98ce\u91c7\u7ae5\u88c5\u300bX bbc X HANAKIMI\u8d8b\u52bf\u53d1\u5e03"
}, {
"id": 233006,
"name": "\u516d\u793c"
}, {
"id": 234321,
"name": "\u8309\u5bfb\/MOUTION"
}, {
"id": 234553,
"name": "\u76c8\u4e91\u79d1\u6280"
}, {
"id": 234554,
"name": "\u6469\u8fea\u83b2\u59ff"
}, {
"id": 234556,
"name": "\u4e91\u6c34\u82b3\u534e"
}, {
"id": 234557,
"name": "\u4f18\u5e03"
}, {
"id": 234560,
"name": "\u7ae0\u9547\u5e03\u827a"
}, {
"id": 234563,
"name": "\u8457\u8863"
}, {
"id": 234570,
"name": "\u5343\u7389\u9999\u7ea6"
}, {
"id": 234571,
"name": "\u65b0\u6750\u6599\u6d41\u884c\u8d8b\u52bf"
}, {
"id": 237235,
"name": "\u4e2d\u534e\u676f\u603b\u51b3\u8d5b"
}, {
"id": 239161,
"name": "\u89c9\u7ec7SENSIYARN"
}, {
"id": 239297,
"name": "\u9b45\u529b\u6696\u57ce\u00b7\u7ed2\u5408\u70ab\u5f69"
}, {
"id": 239303,
"name": "\u53e4\u60a6\u65e5\u5b63"
}, {
"id": 239305,
"name": "\u65f6\u5149\u7ec7\u68a6\u00b7\u7f8a\u7ed2\u534e\u7ae0"
}, {
"id": 239340,
"name": "\u8c37\u66fc\u79cb"
}, {
"id": 239342,
"name": "\u7f8e\u4e3d\u79d8\u5bc6"
}, {
"id": 239343,
"name": "\u540d\u9f20\u7537\u88c5"
}, {
"id": 239344,
"name": "\u96c5\u58eb\u51ef"
}, {
"id": 239346,
"name": "\u6708\u7403"
}, {
"id": 239357,
"name": "\u897f\u7531\u5b9a\u5236"
}, {
"id": 239359,
"name": "\u4e2d\u96cd\u4e91\u7eb1"
}, {
"id": 239362,
"name": "\u5f20\u9896\u83b9 \u00d7 \u4ebf\u7f8e lmate"
}, {
"id": 239365,
"name": "\u5409\u5154\u4ed9"
}, {
"id": 239390,
"name": "\u90fd\u5e02\u65b0\u611f\u89c9"
}, {
"id": 239391,
"name": "\u97e9\u793e"
}, {
"id": 239392,
"name": "\u7c73\u8bd7\u82ac"
}, {
"id": 239393,
"name": "\u7ef4\u591a\u6155"
}, {
"id": 244183,
"name": "\u6d6e\u68a6\u5ba2"
}, {
"id": 244134,
"name": "\u5982\u5b50\u4e4b\u8863"
}, {
"id": 244180,
"name": "\u73ed\u5fb7\u5c3c\u5c14"
}, {
"id": 244182,
"name": "\u5cbd\u65b9\u65e2\u767d"
}, {
"id": 244185,
"name": "\u4e4c\u8499\u6751\u79c0"
}, {
"id": 244188,
"name": "\u74c5\u9526"
}, {
"id": 241144,
"name": "\u76db\u6cfd\u7ec7\u9020"
}, {
"id": 242878,
"name": "\u5e0c\u53ca\u00b7\u4e07\u7269\u767d"
}, {
"id": 244181,
"name": "\u4ee3\u590f"
}, {
"id": 240329,
"name": "\uff08un\uff09decided"
}, {
"id": 240393,
"name": "\u9648\u5217\u5171\u548c"
}, {
"id": 241150,
"name": "\u7d2b\u6676\u82b1 x SHIN"
}, {
"id": 241170,
"name": "\u534e\u5cf0\u5343\u79a7"
}, {
"id": 241601,
"name": "\u805a\u8863\u5802"
}, {
"id": 241794,
"name": "\u70ed\u6c34\u91ce\u4eba"
}, {
"id": 242876,
"name": "\u6b66\u6c49\u767e\u8054\u5965\u7279\u83b1\u65af\u79c0"
}, {
"id": 243101,
"name": "\u65e9\u7a3b\u7530\u5927\u5b66"
}, {
"id": 243124,
"name": "\u7b2c\u4e5d\u5c4a\u4e2d\u56fd\u56fd\u9645\u65f6\u88c5\u8bbe\u8ba1\u521b\u65b0\u4f5c\u54c1\u5927\u8d5b"
}, {
"id": 244056,
"name": "\u66fc\u9640\u5fc3"
}, {
"id": 244130,
"name": "\u5e72\u58eb"
}, {
"id": 244174,
"name": "\u76d0\u8fb9"
}, {
"id": 244175,
"name": "\u8d3a\u82e1\u58a8"
}, {
"id": 244184,
"name": "\u5357\u5c71\u667a\u5c1a\u676f"
}, {
"id": 244192,
"name": "\u7d2b\u6676\u82b1 \u2573 PISKULINA"
}, {
"id": 244216,
"name": "\u5149\u5408\u9ad8\u5b9a"
}, {
"id": 244221,
"name": "\u8543\u5df4\u79c0"
}, {
"id": 244603,
"name": "\u4e16\u827a\u4f1a"
}, {
"id": 244607,
"name": "\u65f6\u5c1a\u6c99\u6eaa\u98ce"
}, {
"id": 244613,
"name": "\u65ed\u65e5\u5ee3\u6771\u670d\u88c5\u5b66\u9662"
}, {
"id": 244654,
"name": "\u4e2d\u56fd\u80e1\u8f69"
}, {
"id": 244886,
"name": "\u6d59\u6c5f\u8d22\u7ecf\u5927\u5b66\u4e1c\u65b9\u5b66\u9662"
}, {
"id": 244928,
"name": "\u6b66\u6c49\u4f53\u80b2\u5b66\u9662"
}, {
"id": 244929,
"name": "\u5185\u8499\u53e4\u827a\u672f\u5b66\u9662"
}, {
"id": 245005,
"name": "\u7b2c 30 \u5c4a\u4e2d\u56fd\u65f6\u88c5\u8bbe\u8ba1\u65b0\u4eba\u5956\u8bc4\u9009"
}, {
"id": 245050,
"name": "\u676d\u5dde\u8f7b\u5de5\u6280\u5e08\u5b66\u9662"
}, {
"id": 245257,
"name": "\u5f20\u69ce\u9488\u7ec7\u96c6\u7fa4\u54c1\u724c\u8054\u5408\u79c0"
}, {
"id": 245735,
"name": "\u6da6\u6052"
}, {
"id": 245747,
"name": "\u7b2c\u4e8c\u5341\u516b\u5c4a\u201c\u771f\u76ae\u6807\u5fd7\u676f\u201d"
}, {
"id": 245782,
"name": "\u7ca4\u6e2f\u6fb3\u5927\u6e7e\u533a\u65f6\u5c1a\u6c47\u6f14"
}, {
"id": 246281,
"name": "\u804a\u57ce\u68c9\u670d"
}, {
"id": 247209,
"name": "\u9752\u5e74\u6bdb\u7ec7\u8bbe\u8ba1\u5e08\u5927\u8d5b"
}, {
"id": 240112,
"name": "\u4e1c\u4eac\u7b2c98\u5c4a\u65f6\u88c5\u5927\u8d5b\u88c5\u82d1\u8d4f"
}, {
"id": 241122,
"name": "\u683c\u6851"
}, {
"id": 241123,
"name": "\u661f\u9645\u201c\u6f2b\u201d\u6b65"
}, {
"id": 241124,
"name": "\u4e0e\u5149\u540c\u822a"
}, {
"id": 241125,
"name": "\u66ae\u5149\u7ec7\u68a6"
}, {
"id": 241126,
"name": "\u6c14\u5019\u9038\u52a8"
}, {
"id": 241579,
"name": "\u886c\u886b\u8001\u7f57"
}, {
"id": 241623,
"name": "\u4e91\u559c\u7eb1\u534e"
}, {
"id": 241795,
"name": "\u516c\u4e3b\u5149\u73af"
}, {
"id": 241931,
"name": "\u4e00\u7740\u00b7\u7ae0\u6d77\u71d5"
}, {
"id": 241936,
"name": "\u53e4\u62d9\u8d28\u9020\/\u6e05\u96c5\u6cfd\u00b7\u53e4\u62d9\u8d28\u9020"
}, {
"id": 242235,
"name": "_J.L-A.L_ x Marzotto"
}, {
"id": 242811,
"name": "\u7f20\u4e4b\u82b1\u5de5\u574a&\u7eba\u5927\u67d3\u8bed"
}, {
"id": 242874,
"name": "\u65b9\u4f9d\u4f9d"
}, {
"id": 242875,
"name": "\u672a\u89e3"
}, {
"id": 242879,
"name": "\u715c\u79c0\u5802"
}, {
"id": 243136,
"name": "\u6df1\u5733\u529b\u91cf\u65f6\u88c5\u8bbe\u8ba1\u5e08\u4f5c\u54c1\u8054\u5408\u53d1\u5e03"
}, {
"id": 243137,
"name": "\u7ca4\u6e2f\u6fb3\u5927\u6e7e\u533a\u540d\u5e08\u4f5c\u54c1\u8054\u5408\u53d1\u5e03"
}, {
"id": 244186,
"name": "\u5a74\u4e8c\u4ee3"
}, {
"id": 244187,
"name": "\u4e2d\u6e2f\u76ae\u5177\u57ce"
}, {
"id": 244189,
"name": "\u6e7e\u533a\u9999\u4e91\u7eb1\u9762\u6599\u7814\u53d1\u4e2d\u5fc3"
}, {
"id": 244191,
"name": "\u5fae\u5149\u5c9b"
}, {
"id": 244218,
"name": "\u827e\u831c\u5a1c\u53e4\u7ee3\u5e84"
}, {
"id": 244219,
"name": "\u4fdd\u5170\u5fb7&\u53cb\u8c0a\u4f7f\u8005"
}, {
"id": 244220,
"name": "\u521d\u526a"
}, {
"id": 244222,
"name": "\u5357\u65b9\u7425\u73c0"
}, {
"id": 244223,
"name": "\u6e05\u541b"
}, {
"id": 244471,
"name": "\u4e94\u6307\u5c71Miss\u9ece\u5927\u79c0"
}, {
"id": 244507,
"name": "\u4e2d\u56fd\u65d7\u888d\u00b7\u9ece\u97f5"
}, {
"id": 244574,
"name": "\u6f6e\u73a9\u65f6\u88c5\u8de8\u754c\u5148\u950b\u8bbe\u8ba1\u5e08"
}, {
"id": 244615,
"name": "\u5c15\u5a03\u5566"
}, {
"id": 244633,
"name": "\u5e7f\u6e05\u7eba\u7ec7\u56ed"
}, {
"id": 245051,
"name": "\u676d\u5dde\u7f8e\u672f\u804c\u4e1a\u5b66\u6821"
}, {
"id": 245734,
"name": "\u76ae\u57ce\u4e25\u9009\u54c1\u724c\u8054\u5408"
}, {
"id": 245736,
"name": "\u5723\u739b\u4e01\u56fd\u9645\u65f6\u88c5\u5468\u73ed"
}, {
"id": 246208,
"name": "\u5f20\u6d2a"
}, {
"id": 246247,
"name": "\u97e9\u5b9c"
}, {
"id": 246956,
"name": "\u8bd1\u4f9d"
}, {
"id": 247210,
"name": "\u9999\u96f2\u6613\u898b"
}, {
"id": 247230,
"name": "\u6c49\u9526\u96c6"
}, {
"id": 247277,
"name": "\u91d1\u5b9d\u4e2d\u5f0f"
}]

27
codeception.yml Normal file
View File

@ -0,0 +1,27 @@
actor: Tester
bootstrap: _bootstrap.php
paths:
tests: tests
output: tests/_output
data: tests/_data
helpers: tests/_support
settings:
memory_limit: 1024M
colors: true
modules:
config:
Yii2:
configFile: 'config/test.php'
# To enable code coverage:
#coverage:
# #c3_url: http://localhost:8080/index-test.php/
# enabled: true
# #remote: true
# #remote_config: '../codeception.yml'
# whitelist:
# include:
# - models/*
# - controllers/*
# - commands/*
# - mail/*

View File

@ -0,0 +1,137 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace app\commands;
use app\common\FileHelper;
use app\common\SaltHelper;
use app\enums\SourceEnum;
use app\models\Brand;
use app\models\BrandAlias;
use app\models\BrandRunwayImages;
use app\models\BrandSource;
use app\models\logics\commands\SpiderVogue;
use app\models\User;
use Qiniu\Auth;
use Qiniu\Storage\UploadManager;
use yii\console\Controller;
use yii\console\ExitCode;
/**
* This command echoes the first argument that you have entered.
*
* This command is provided as an example for you to learn how to create console commands.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class HelloController extends Controller
{
/**
* This command echoes what you have entered as the message.
* @param string $message the message to be echoed.
* @return int Exit code
*/
public function actionIndex($message = 'hello world')
{
foreach(BrandRunwayImages::find()->each() as $item) {
var_dump($item['image']);
$cont = file_get_contents($item['image']);
$imageName = md5(SaltHelper::addSalt($item['image']));
$imagePath = md5(SaltHelper::addSalt($item['runway_id']));
if (!is_dir(\Yii::getAlias('@runtime') . "/{$imagePath}")) {
FileHelper::createDirectory(\Yii::getAlias('@runtime') . "/{$imagePath}");
}
file_put_contents(\Yii::getAlias('@runtime') . "/{$imagePath}/{$imageName}", $cont);
$accessKey = '7GLHrN7BqrI9JnWZ9Pki2q5rqPazhIFroo19a-Av';
$secretKey = 'gmg6PLgg666Cme-gsyTlsBLhshDv-6_zsEmW4jRY';
$auth = new Auth($accessKey, $secretKey);
$bucket = '23cm';
// 生成上传Token
$token = $auth->uploadToken($bucket);
// 要上传文件的本地路径
$filePath = \Yii::getAlias('@runtime') . "/{$imagePath}/{$imageName}";
// 上传到存储后保存的文件名
$key = "{$imagePath}/{$imageName}";
// 初始化 UploadManager 对象并进行文件的上传。
$uploadMgr = new UploadManager();
// 调用 UploadManager 的 putFile 方法进行文件的上传。
list($ret, $err) = $uploadMgr->putFile($token, $key, $filePath, null, 'application/octet-stream', true, null, 'v2');
echo "\n====> putFile result: \n";
if ($err !== null) {
var_dump($err);
} else {
var_dump($ret);
}
// 构建 UploadManager 对象
// $uploadMgr = new UploadManager();
$qu = BrandRunwayImages::findOne($item['id']);
$qu->name = $key;
$qu->save();
// die;
}
die;
foreach(BrandSource::find()->each() as $item) {
$brandName = Brand::find()->where(['id' => $item['brand_id']])->one()->name;
$brandName = strtr($brandName, [
' ' => '-',
'.' => '-'
]);
var_dump(strtolower($brandName));
$q = BrandSource::findOne($item['id']);
$q->source_url = '/fashion-shows/designer/'.strtolower($brandName);
$q->save();
}
die;
$model = new SpiderVogue();
$model->setBrandName('gucci');
var_dump($model->start());
die;
ini_set('memory_limit', '1024M');
$content = file_get_contents(\Yii::$app->basePath . '/brand.txt');
var_dump($content);
$content = (json_decode($content, true));
// var_dump(json_last_error());
// var_dump(json_last_error_msg());
// var_dump($content);die;
foreach ($content as $item) {
$model = new Brand();
if (stripos($item['name'], '/') !== false) {
$alias = explode('/', $item['name']);
foreach ($alias as $i => $aliasItem) {
if ($i == 0) {
$model->name = $aliasItem;
$model->show_name = $item['name'];
$model->save();
$lastId = $model->id;
var_dump($model->errors);
} else {
$aliasModel = new BrandAlias();
$aliasModel->brand_id = $lastId;
$aliasModel->name = $aliasItem;
$aliasModel->save();
var_dump($aliasModel->errors);
}
}
} else {
$model->name = $item['name'];
$model->show_name = $item['name'];
$model->save();
// var_dump($model->errors);
}
}
return ExitCode::OK;
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace app\commands;
use app\common\CommonHelper;
use app\common\CurlApp;
use app\common\MobileHelper;
use app\models\Brand;
use app\models\BrandRunway;
use app\models\BrandSource;
use app\models\Clue;
use app\models\jobs\RunwayJob;
use app\models\logics\commands\SpiderVogue;
use app\models\Oauth;
use app\models\OauthAccount;
use app\models\OauthAccountLocal;
use app\models\User;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use yii\console\Controller;
use yii\console\ExitCode;
use GuzzleHttp\Exception\RequestException;
use yii\helpers\ArrayHelper;
/**
* This command echoes the first argument that you have entered.
*
* This command is provided as an example for you to learn how to create console commands.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class SpiderController extends Controller
{
public function __construct($id, $module, $config = [])
{
ini_set('pcre.backtrack_limit', '-1');
parent::__construct($id, $module, $config);
}
public function actionVogue()
{
$model = new SpiderVogue();
$model->start();
}
public function actionRunway()
{
foreach (BrandSource::find()->where(['is_deleted' => 0])->each() as $index => $item) {
if ($index == 1) {
\Yii::$app->queue->push(new RunwayJob([
'source' => $item,
]));die;
}
}
}
}

13
common/CommonHelper.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace app\common;
class CommonHelper
{
public static function getYear(string $title)
{
preg_match('/([0-9]+)/', $title, $y);
return $y[1] ?? date('Y');
}
}

167
common/CurlApp.php Normal file
View File

@ -0,0 +1,167 @@
<?php
namespace app\common;
class CurlApp
{
private $ch = null;
private array $options = [];
private string $headerContentType = '';
private string $error = '';
const METHOD_POST = 'POST';
const METHOD_GET = 'GET';
const CONTENT_TYPE_JSON = 'Content-Type: application/json';
const CONTENT_TYPE_FORM_URLENCODED = 'Content-Type: application/x-www-form-urlencoded';
public function __construct()
{
$this->ch = curl_init();
$this->options[CURLOPT_CONNECTTIMEOUT] = 30;
$this->options[CURLOPT_TIMEOUT] = 30;
}
public function setUrl(string $url)
{
$this->options[CURLOPT_URL] = $url;
return $this;
}
public function setCookie(string $cookie)
{
$this->options[CURLOPT_COOKIE] = $cookie;
return $this;
}
public function setReturnTransfer(bool $isReturn = true)
{
$this->options[CURLOPT_RETURNTRANSFER] = $isReturn;
return $this;
}
public function setPostData($data)
{
$this->options[CURLOPT_POSTFIELDS] = $data;
if ($this->headerContentType === self::CONTENT_TYPE_FORM_URLENCODED) {
if (is_array($data)) {
$this->options[CURLOPT_POSTFIELDS] = http_build_query($data);
}
} elseif ($this->headerContentType === self::CONTENT_TYPE_JSON) {
if (is_array($data)) {
$this->options[CURLOPT_POSTFIELDS] = json_encode($data);
} else {
$this->options[CURLOPT_POSTFIELDS] = ($data);
}
}
return $this;
}
public function setHttpHeader(array $header = [])
{
foreach ($header as $key => $value) {
$this->options[CURLOPT_HTTPHEADER][$key] = $value;
}
return $this;
}
public function setProxy($ip, $port)
{
// 基本代理
$this->options[CURLOPT_PROXY] = "{$ip}:{$port}";
// 指定代理类型可选HTTP、SOCKS4、SOCKS5
$this->options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP;
}
public function setContentType($contentType)
{
$this->headerContentType = $contentType;
$this->options[CURLOPT_HTTPHEADER][] = $contentType;
return $this;
}
public function setMethod($method = 'GET')
{
$this->options[CURLOPT_POST] = !(strtolower($method) == 'get');
return $this;
}
public function execMulti($urls = []): array
{
$multiHandle = curl_multi_init();
$handles = [];
$responses = [];
foreach ($urls as $key => $url) {
$ch = curl_init();
$this->options[CURLOPT_CONNECTTIMEOUT] = 60;
$this->options[CURLOPT_TIMEOUT] = 60;
// 继承当前 CurlApp 的 options
$options = $this->options;
$options[CURLOPT_URL] = $url;
$options[CURLOPT_RETURNTRANSFER] = true;
curl_setopt_array($ch, $options);
curl_multi_add_handle($multiHandle, $ch);
$handles[$key] = $ch;
}
// 执行
$running = null;
do {
$status = curl_multi_exec($multiHandle, $running);
if ($running) {
curl_multi_select($multiHandle, 1);
}
} while ($running && $status === CURLM_OK);
// 收集结果
foreach ($handles as $key => $ch) {
$responses[$key] = curl_multi_getcontent($ch);
curl_multi_remove_handle($multiHandle, $ch);
curl_close($ch);
}
curl_multi_close($multiHandle);
return $responses;
}
public function exec()
{
$defaultOptions = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYHOST => false,
];
foreach ($defaultOptions as $key => $val) {
if (!isset($this->options[$key])) {
$this->options[$key] = $val;
}
}
curl_setopt_array($this->ch, $this->options);
$resp = curl_exec($this->ch);
if (curl_errno($this->ch)) {
$this->error = curl_error($this->ch);
return false;
}
curl_close($this->ch);
return $resp;
}
public function getError()
{
return $this->error;
}
}

283
common/FileHelper.php Normal file
View File

@ -0,0 +1,283 @@
<?php
namespace app\common;
use yii\helpers\BaseFileHelper;
use Yii;
/**
* Class FileHelper
* @package common\helpers
* @author jianyan74 <751393839@qq.com>
*/
class FileHelper extends BaseFileHelper
{
public $_speed = 0;
/**
* 检测目录并循环创建目录
*
* @param $catalogue
*/
public static function mkdirs($catalogue)
{
if (!file_exists($catalogue)) {
self::mkdirs(dirname($catalogue));
mkdir($catalogue, 0777);
exec('chown -R www.www '.$catalogue);
}
return true;
}
/**
* 写入日志
*
* @param $path
* @param $content
* @return bool|int
*/
public static function writeLog($path, $content)
{
$use_num = self::sysProbe();
// $use_num = Yii::$app->redis->get(CacheKeyEnum::SYSTEM_PROBE);
if ($use_num > 98) {
return false;
}
self::mkdirs(dirname($path));
return file_put_contents($path, date('Y-m-d H:i:s') . ">>>" . $content . "\r\n", FILE_APPEND);
}
/**
* 检测磁盘空间
* @return bool|int|string
*/
public static function sysProbe()
{
$total = round(@disk_total_space(".") / (1024 * 1024 * 1024), 2);
$free = round(@disk_free_space(".") / (1024 * 1024 * 1024), 2);
$use_num = intval($total - $free) ? : 1;
// exec("df -h", $systemInfo);
// $disk = $systemInfo[5] ?? $systemInfo[1]; //没有找到其他磁盘就默认第一个
// $use_num = 1;
// if ($disk) {
// $use_num = trim(substr($disk, -10, -7)) ? : 1; //媒介保后端服务器磁盘用量
// }
// Yii::$app->redis->setex(CacheKeyEnum::SYSTEM_PROBE, 3600, $use_num);
return $use_num;
}
/**
* 获取文件夹大小
*
* @param string $dir 根文件夹路径
* @return int
*/
public static function getDirSize($dir)
{
$handle = opendir($dir);
$sizeResult = 0;
while (false !== ($FolderOrFile = readdir($handle))) {
if ($FolderOrFile != "." && $FolderOrFile != "..") {
if (is_dir("$dir/$FolderOrFile")) {
$sizeResult += self::getDirSize("$dir/$FolderOrFile");
}
else {
$sizeResult += filesize("$dir/$FolderOrFile");
}
}
}
closedir($handle);
return $sizeResult;
}
/**
* 基于数组创建目录
*
* @param $files
*/
public static function createDirOrFiles($files)
{
foreach ($files as $key => $value) {
if (substr($value, -1) == '/') {
mkdir($value);
}
else {
file_put_contents($value, '');
}
}
}
/**
* 文件大小字节转换对应的单位
* @param $size
* @return string
*/
public static function convert($size)
{
$unit = array('b','kb','MB','GB','tb','pb');
return round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . '' . $unit[$i];
}
public static function downloadVideo($url)
{
// $is_url = Url::isUrl($url);
// if (!$is_url) {
// exit('不是正确的链接地址'); //exit掉下载下来打开会显示无法播放格式不支持文件已损坏等
// }
//获取文件信息
// $fileExt = pathinfo($url);
//获取文件的扩展名
// $allowDownExt = array ('mp4', 'mov');
//检测文件类型是否允许下载
// if (!in_array($fileExt['extension'], $allowDownExt)) {
// exit('不支持该格式');
// }
// 设置浏览器下载的文件名,这里还以原文件名一样
$filename = basename($url);
// 获取远程文件大小
// 注意filesize()无法获取远程文件大小
$headers = get_headers($url, 1);
$fileSize = $headers['Content-Length'];
if (ini_get('zlib.output_compression')) {
ini_set('zlib.output_compression', 'Off');
}
header_remove('Content-Encoding');
// 设置header头
// 因为不知道文件是什么类型的,告诉浏览器输出的是字节流
header('Content-Type: application/octet-stream');
// 告诉浏览器返回的文件大小类型是字节
header('Accept-Ranges:bytes');
// 告诉浏览器返回的文件大小
header('Content-Length: ' . $fileSize);
// 告诉浏览器文件作为附件处理并且设定最终下载完成的文件名称
header('Content-Disposition: attachment; filename="' . $filename . '"');
//针对大文件规定每次读取文件的字节数为4096字节直接输出数据
$read_buffer = 4096; //4096
$handle = fopen($url, 'rb');
//总的缓冲的字节数
$sum_buffer = 0;
//只要没到文件尾,就一直读取
while (!feof($handle) && $sum_buffer < $fileSize) {
echo fread($handle, $read_buffer);
$sum_buffer += $read_buffer;
}
fclose($handle);
exit;
}
/**
* @param String $file 要下载的文件路径
* @param String $name 文件名称,为空则与下载的文件名称一样
* @param boolean $reload 是否开启断点续传
* @return string
*/
public static function downloadFile($file, $name = '', $reload = false)
{
$log_path = Yii::getAlias('@runtime') . '/api/' . date('Ym') . '/' . date('d') . '/download.txt';
FileHelper::writeLog($log_path, $file);
$fp = fopen($file, 'rb');
if ($fp) {
if ($name == '') {
$name = basename($file);
}
$header_array = get_headers($file, true);
// 下载本地文件,获取文件大小
if (!$header_array) {
$file_size = filesize($file);
} else {
$file_size = $header_array['Content-Length'];
}
FileHelper::writeLog($log_path, json_encode($_SERVER, JSON_UNESCAPED_UNICODE));
if (isset($_SERVER['HTTP_RANGE']) && !empty($_SERVER['HTTP_RANGE'])) {
$ranges = self::getRange($file_size);
} else {
//第一次连接
$size2 = $file_size - 1;
header("Content-Range: bytes 0-$size2/$file_size"); //Content-Range: bytes 0-4988927/4988928
header("Content-Length: " . $file_size); //输出总长
}
$ua = $_SERVER["HTTP_USER_AGENT"];//判断是什么类型浏览器
header('cache-control:public');
header('content-type:application/octet-stream');
$encoded_filename = urlencode($name);
$encoded_filename = str_replace("+", "%20", $encoded_filename);
//解决下载文件名乱码
if (preg_match("/MSIE/", $ua) || preg_match("/Trident/", $ua)) {
header('Content-Disposition: attachment; filename="' . $encoded_filename . '"');
} else if (preg_match("/Firefox/", $ua)) {
header('Content-Disposition: attachment; filename*="utf8\'\'' . $name . '"');
} else if (preg_match("/Chrome/", $ua)) {
header('Content-Disposition: attachment; filename="' . $encoded_filename . '"');
} else {
header('Content-Disposition: attachment; filename="' . $name . '"');
}
//header('Content-Disposition: attachment; filename="' . $name . '"');
if ($reload && $ranges != null) { // 使用续传
header('HTTP/1.1 206 Partial Content');
header('Accept-Ranges:bytes');
// 剩余长度
header(sprintf('content-length:%u', $ranges['end'] - $ranges['start']));
// range信息
header(sprintf('content-range:bytes %s-%s/%s', $ranges['start'], $ranges['end'], $file_size));
FileHelper::writeLog($log_path, sprintf('content-length:%u', $ranges['end'] - $ranges['start']));
// fp指针跳到断点位置
fseek($fp, sprintf('%u', $ranges['start']));
} else {
header('HTTP/1.1 200 OK');
header('content-length:' . $file_size);
}
while (!feof($fp)) {
echo fread($fp, 4096);
ob_flush();
}
($fp != null) && fclose($fp);
} else {
return '';
}
}
/** 设置下载速度
* @param int $speed
*/
public function setSpeed($speed)
{
if (is_numeric($speed) && $speed > 16 && $speed < 4096) {
$this->_speed = $speed;
}
}
/** 获取header range信息
* @param int $file_size 文件大小
* @return Array
*/
private static function getRange($file_size)
{
if (isset($_SERVER['HTTP_RANGE']) && !empty($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE'];
$range = preg_replace('/[\s|,].*/', '', $range);
$range = explode('-', substr($range, 6));
if (count($range) < 2) {
$range[1] = $file_size;
}
$range = array_combine(array('start','end'), $range);
if (empty($range['start'])) {
$range['start'] = 0;
}
if (empty($range['end'])) {
$range['end'] = $file_size;
}
return $range;
}
return null;
}
}

16
common/ImageHelper.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace app\common;
class ImageHelper
{
public static function imageMogr2H480(string $imageUrl): string
{
return "{$imageUrl}-h480";
}
public static function imageMogr2H1080(string $imageUrl): string
{
return "{$imageUrl}-h1080";
}
}

18
common/MobileHelper.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace app\common;
class MobileHelper
{
public static function isValidChinaMobile($remarkDict): bool
{
if (is_string($remarkDict) && $remarkDict) {
$val = json_decode($remarkDict, true);
if (isset($val['是否为隐私号']) && $val['是否为隐私号'] == '是') {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace app\common;
use yii\data\ActiveDataProvider;
use yii\data\ArrayDataProvider;
use yii\db\ActiveQuery;
use yii\db\QueryInterface;
use yii\di\NotInstantiableException;
class PaginationHelper
{
/**
*
* @param \yii\db\ActiveQuery $query
* @param int $limit
* @param string $pageParam
* @param string $pageSizeParam
*
* @return \yii\data\ActiveDataProvider
*/
public static function createDataProvider($query, $limit = 10, $pageParam = 'page', $pageSizeParam = 'per_page')
{
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
//分页大小
'pageSize' => \Yii::$app->request->get($pageSizeParam) ?: $limit,
//设置地址栏当前页数参数名
'pageParam' => $pageParam,
//设置地址栏分页大小参数名
'pageSizeParam' => $pageSizeParam,
],
]);
return $dataProvider;
}
/**
* @param $query
* @param int $limit
* @param string $pageParam
* @param string $pageSizeParam
*
* @return \yii\db\QueryInterface
* @throws \yii\di\NotInstantiableException
*/
public static function createQueryProvider($query, $limit = 10, $pageParam = 'page', $pageSizeParam = 'limit')
{
$param = \Yii::$app->request->get();
$pageSize = $param['limit'] ?: $param[$pageSizeParam];
if ($query instanceof QueryInterface) {
return $query->limit($pageSize)->offset(($param[$pageParam] - 1) * $pageSize);
}
throw new NotInstantiableException('not instanceof QueryInterFace');
}
/**
* @param array $array
* @param int $limit
* @param string $pageParam
* @param string $pageSizeParam
* @param array $orderFields
*
* @return ArrayDataProvider
*/
public static function createArrayDataProvider($array = [], $orderFields = [], $limit = 10, $pageParam = 'page', $pageSizeParam = 'limit')
{
$attributes = array_keys($orderFields);
$order = $orderFields;
$dataProvider = new ArrayDataProvider([
'allModels' => $array,
'sort' => [
'attributes' => $attributes,
'defaultOrder' => $order
],
'pagination' => [
//分页大小
'pageSize' => \Yii::$app->request->get($pageSizeParam, 10),
//设置地址栏当前页数参数名
'pageParam' => $pageParam,
//设置地址栏分页大小参数名
'pageSizeParam' => $pageSizeParam,
],
]);
return $dataProvider;
}
}

11
common/SaltHelper.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace app\common;
class SaltHelper
{
public static function addSalt(string $data)
{
return $data . \Yii::$app->params['salt'];
}
}

106
components/Oceanengine.php Normal file
View File

@ -0,0 +1,106 @@
<?php
namespace app\components;
use app\models\Oauth;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use yii\base\Component;
class Oceanengine extends Component
{
const CACHE_KEY = 'oceanengine_cache';
public function getAccessToken($adminUid)
{
return Oauth::find()->where(['uid' => $adminUid])->one()->access_token;
}
public function getRefreshToken($adminUid)
{
return Oauth::find()->where(['uid' => $adminUid])->one()->refresh_token;
}
/**
*
* 获取授权账号下的子账号
* @param $adminAccountId
*
* @return array
*/
public function getAdminChildAccount($adminAccountId): array
{
$client = new Client();
$headers = [
'Access-Token' => $this->getAccessToken($adminAccountId)
];
$request = new Request('GET',
'https://api.oceanengine.com/open_api/oauth2/advertiser/get/',
$headers);
$res = $client->sendAsync($request)->wait();
return json_decode($res->getBody(), true);
}
/**
* 获取本地推用户
*
* @param $adminUid
*
* @return array
*/
public function getAccountLocal($adminUid): array
{
$client = new Client();
$headers = [
'Access-Token' => $this->getAccessToken($adminUid)
];
$request = new Request('GET', 'https://ad.oceanengine.com/open_api/2/customer_center/advertiser/list/?account_source=LOCAL&cc_account_id=1742303335399432&filtering=%7B%22account_name%22%3A%22%22%7D&page=1&page_size=100', $headers);
$res = $client->sendAsync($request)->wait();
return json_decode($res->getBody(), true);
}
/**
* 获取最新的线索
*/
public function getClue($adminUid, array $accountId, $startTime, $endTime, $page = 1)
{
$client = new Client();
$headers = [
'Content-Type' => 'application/json',
'Access-Token' => $this->getAccessToken($adminUid)
];
$accounts = implode(',', $accountId);
$body = '{
"local_account_ids": [
' . $accounts . '
],
"start_time": "' . $startTime . '",
"end_time": "' . $endTime . '",
"page": ' . $page . ',
"page_size": 100
}';
$request = new Request('POST', 'https://api.oceanengine.com/open_api/2/tools/clue/life/get/', $headers, $body);
$res = $client->sendAsync($request)->wait();
return json_decode($res->getBody(), true);
}
public function refreshAccessToken($adminUid)
{
$client = new Client();
$headers = [
'Content-Type' => 'application/json'
];
$body = '{
"app_id": ' . \Yii::$app->params['app_id'] . ',
"secret": "' . \Yii::$app->params['secret'] . '",
"refresh_token": "' . $this->getRefreshToken($adminUid) . '"
}';
$request = new Request('POST',
'https://api.oceanengine.com/open_api/oauth2/refresh_token/',
$headers, $body);
$res = $client->sendAsync($request)->wait();
return json_decode($res->getBody(), true);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace app\components\serializers;
class Serializer
{
public function __construct(
public array $rules = []
)
{
}
public function serialize()
{
}
}

82
composer.json Normal file
View File

@ -0,0 +1,82 @@
{
"name": "yiisoft/yii2-app-basic",
"description": "Yii 2 Basic Project Template",
"keywords": ["yii2", "framework", "basic", "project template"],
"homepage": "https://www.yiiframework.com/",
"type": "project",
"license": "BSD-3-Clause",
"minimum-stability": "stable",
"require": {
"php": ">=7.4.0",
"yiisoft/yii2": "~2.0.45",
"yiisoft/yii2-bootstrap5": "~2.0.2",
"yiisoft/yii2-symfonymailer": "~2.0.3",
"yidas/yii2-composer-bower-skip": "^2.0",
"yidas/yii2-bower-asset": "^2.0",
"guzzlehttp/guzzle": "^7.0",
"omnilight/yii2-scheduling": "*",
"yiisoft/yii2-queue": "^2.3",
"yiisoft/yii2-redis": "^2.1",
"qiniu/php-sdk": "^7.14"
},
"require-dev": {
"yiisoft/yii2-debug": "~2.1.0",
"yiisoft/yii2-gii": "~2.2.0",
"yiisoft/yii2-faker": "~2.0.0",
"codeception/codeception": "^5.0.0 || ^4.0",
"codeception/lib-innerbrowser": "^4.0 || ^3.0 || ^1.1",
"codeception/module-asserts": "^3.0 || ^1.1",
"codeception/module-yii2": "^1.1",
"codeception/module-filesystem": "^3.0 || ^2.0 || ^1.1",
"codeception/verify": "^3.0 || ^2.2",
"symfony/browser-kit": "^6.0 || >=2.7 <=4.2.4"
},
"config": {
"process-timeout": 1800,
"allow-plugins": {
"yiisoft/yii2-composer": true
},
"fxp-asset": {
"enabled": false
}
},
"scripts": {
"post-install-cmd": [
"yii\\composer\\Installer::postInstall"
],
"post-create-project-cmd": [
"yii\\composer\\Installer::postCreateProject",
"yii\\composer\\Installer::postInstall"
]
},
"extra": {
"yii\\composer\\Installer::postCreateProject": {
"setPermission": [
{
"runtime": "0777",
"web/assets": "0777",
"yii": "0755"
}
]
},
"yii\\composer\\Installer::postInstall": {
"generateCookieValidationKey": [
"config/web.php"
]
}
},
"repositories": [
{
"type": "composer",
"url": "https://mirrors.cloud.tencent.com/composer/"
}
]
}

5100
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

33
config/__autocomplete.php Normal file
View File

@ -0,0 +1,33 @@
<?php
/**
* This class only exists here for IDE (PHPStorm/Netbeans/...) autocompletion.
* This file is never included anywhere.
* Adjust this file to match classes configured in your application config, to enable IDE autocompletion for custom components.
* Example: A property phpdoc can be added in `__Application` class as `@property \vendor\package\Rollbar|__Rollbar $rollbar` and adding a class in this file
* ```php
* // @property of \vendor\package\Rollbar goes here
* class __Rollbar {
* }
* ```
*/
class Yii {
/**
* @var \yii\web\Application|\yii\console\Application|__Application
*/
public static $app;
}
/**
* @property yii\rbac\DbManager $authManager
* @property \yii\web\User|__WebUser $user
*
*/
class __Application {
}
/**
* @property app\models\User $identity
*/
class __WebUser {
}

72
config/console.php Normal file
View File

@ -0,0 +1,72 @@
<?php
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/db.php';
$config = [
'id' => 'basic-console',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log', 'queue'],
'controllerNamespace' => 'app\commands',
'timeZone' => 'Asia/Shanghai',
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
'@tests' => '@app/tests',
],
'components' => [
'queue' => [
'class' => \yii\queue\redis\Queue::class,
'redis' => 'redis', // Redis 连接组件或其配置
'channel' => 'queue', // 队列通道键
'as log' => \yii\queue\LogBehavior::class, // 日志行为
],
'oceanengine' => [
'class' => 'app\components\Oceanengine',
],
'redis' => [
'class' => 'yii\redis\Connection',
'hostname' => 'localhost',
'port' => 6379,
'database' => 0,
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'db' => $db,
],
'params' => $params,
/*
'controllerMap' => [
'fixture' => [ // Fixture generation command line.
'class' => 'yii\faker\FixtureController',
],
],
*/
];
if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
// $config['bootstrap'][] = 'gii';
// $config['modules']['gii'] = [
// 'class' => 'yii\gii\Module',
// ];
// configuration adjustments for 'dev' environment
// requires version `2.1.21` of yii2-debug module
$config['bootstrap'][] = 'debug';
$config['modules']['debug'] = [
'class' => 'yii\debug\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];
}
return $config;

15
config/db.php Normal file
View File

@ -0,0 +1,15 @@
<?php
return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=23cm',
'username' => 'root',
'password' => '123456',
'charset' => 'utf8',
// mysql_77BFSX
// Schema cache options (for production environment)
//'enableSchemaCache' => true,
//'schemaCacheDuration' => 60,
//'schemaCache' => 'cache',
];

14
config/params.php Normal file
View File

@ -0,0 +1,14 @@
<?php
return [
'adminEmail' => 'admin@example.com',
'senderEmail' => 'noreply@example.com',
'senderName' => 'Example.com mailer',
'app_id' => '1852484891502756',
'secret' => '06b3ab42e212f4824fa19d958512d025eb477fea',
'salt' => 'toom1996',
'cdnAddress' => 'http://static.23cm.cn/'
];

18
config/schedule.php Normal file
View File

@ -0,0 +1,18 @@
<?php
/**
* @var \omnilight\scheduling\Schedule $schedule
*/
// /Users/toom/Documents/yii2/xiansuo_admin
// php /www/wwwroot/sites/clue/index/clue/yii schedule/run --scheduleFile=/Users/toom/Documents/yii2/xiansuo_admin/config/schedule.php 1>> /dev/null 2>&1
$path = Yii::getAlias('@runtime') . '/logs/' . date('Ym') . '/' . date('d') . '/';
$h = date('H');
$hi = date('H-i');
\app\common\FileHelper::mkdirs($path);
// php /www/wwwroot/sites/clue/index/clue/yii schedule/run --scheduleFile=/www/wwwroot/sites/clue/index/clue/config/schedule.php 1>> /dev/null 2>&1
/** 8-22/每2小时执行一次 / 获取【小红书定制】文章信息 */
$filePath = $path . 'clue_chain.log';
$schedule->command('sync/pull-clue-chain')->cron('*/5 * * * * *')->appendOutputTo($filePath);
$filePath = $path . 'refresh_token.log';
$schedule->command('sync/refresh-token')->cron('0 */12 * * * *')->appendOutputTo($filePath);

46
config/test.php Normal file
View File

@ -0,0 +1,46 @@
<?php
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/test_db.php';
/**
* Application configuration shared by all test types
*/
return [
'id' => 'basic-tests',
'basePath' => dirname(__DIR__),
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
],
'language' => 'en-US',
'components' => [
'db' => $db,
'mailer' => [
'class' => \yii\symfonymailer\Mailer::class,
'viewPath' => '@app/mail',
// send all mails to a file by default.
'useFileTransport' => true,
'messageClass' => 'yii\symfonymailer\Message'
],
'assetManager' => [
'basePath' => __DIR__ . '/../web/assets',
],
'urlManager' => [
'showScriptName' => true,
],
'user' => [
'identityClass' => 'app\models\User',
],
'request' => [
'cookieValidationKey' => 'test',
'enableCsrfValidation' => false,
// but if you absolutely need it set cookie domain to localhost
/*
'csrfCookie' => [
'domain' => 'localhost',
],
*/
],
],
'params' => $params,
];

6
config/test_db.php Normal file
View File

@ -0,0 +1,6 @@
<?php
$db = require __DIR__ . '/db.php';
// test database! Important not to run tests on production or development databases
$db['dsn'] = 'mysql:host=localhost;dbname=yii2basic_test';
return $db;

96
config/web.php Normal file
View File

@ -0,0 +1,96 @@
<?php
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/db.php';
$config = [
'id' => 'basic',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'aliases' => [
'@bower' => '@vendor/yidas/yii2-bower-asset/bower',
'@npm' => '@vendor/npm-asset',
],
'timeZone' => 'Asia/Shanghai',
'components' => [
'assetManager' => [
// 'bundles' => [
// 'yii\web\JqueryAsset' => [
// 'basePath' => '@webroot',
// 'baseUrl' => '@web',
// 'js' => [
// 'js/jquery.js', // 你的 jQuery
// ],
// ],
// ],
],
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '0VfdiVtgFI4CJ4PNusreE5khkMp8fna2',
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
'oceanengine' => [
'class' => 'app\components\Oceanengine',
],
'user' => [
'identityClass' => 'app\models\User',
'enableAutoLogin' => true,
],
'redis' => [
'class' => 'yii\redis\Connection',
'hostname' => 'localhost',
'port' => 6379,
'database' => 0,
],
'errorHandler' => [
'errorAction' => 'site/error',
],
'mailer' => [
'class' => \yii\symfonymailer\Mailer::class,
'viewPath' => '@app/mail',
// send all mails to a file by default.
'useFileTransport' => true,
],
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'db' => $db,
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
// '<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>', // 带 ID
'<action:\w+>' => 'site/<action>', // site 控制器下的简单 action例如 login, logout, console
'' => 'site/index', // 首页
],
],
],
'params' => $params,
];
if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
// $config['bootstrap'][] = 'debug';
// $config['modules']['debug'] = [
// 'class' => 'yii\debug\Module',
// // uncomment the following to add your IP if you are not connecting from localhost.
// //'allowedIPs' => ['127.0.0.1', '::1'],
// ];
$config['bootstrap'][] = 'gii';
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];
}
return $config;

View File

@ -0,0 +1,286 @@
<?php
namespace app\controllers;
use app\common\PaginationHelper;
use app\models\Clue;
use app\models\Oauth;
use app\models\OauthAccount;
use app\models\OauthAccountLocal;
use app\models\UserAdvertiser;
use app\models\Xiansuo;
use http\Url;
use Yii;
use yii\data\Pagination;
use yii\filters\AccessControl;
use yii\helpers\ArrayHelper;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\VerbFilter;
use app\models\LoginForm;
use app\models\ContactForm;
use yii\web\UrlManager;
class ApiController extends Controller
{
public function beforeAction($action)
{
parent::beforeAction($action);
if (Yii::$app->user->isGuest) {
return $this->redirect('/login');
}
return true;
}
/**
* {@inheritdoc}
*/
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
/**
*
*
* @return Response
*/
public function actionXiansuo()
{
$query = Clue::find()->filterWhere(
['like', 'telephone', Yii::$app->request->get('phone')],
)->andFilterWhere(
['like', 'name', Yii::$app->request->get('name')]
)->andFilterWhere(
['like', 'note', Yii::$app->request->get('note')]
)->andFilterWhere(
['convert_status' => Yii::$app->request->get('convert_status')]
)->andFilterWhere(
['like', 'auto_city_name', Yii::$app->request->get('city')]
)->andFilterWhere(
[ '>=', 'create_time_detail', Yii::$app->request->get('date_start')]
)->andFilterWhere(
[ '<=', 'create_time_detail', Yii::$app->request->get('date_end')]
)->orderBy('create_time_detail DESC');
$provider = PaginationHelper::createDataProvider($query, limit: Yii::$app->request->get('limit'));
$covertStatus = function ($status) {
if ($status == 1) {
return '合法转化';
} elseif ($status == 2) {
return '待确认';
} elseif ($status == 3) {
return '营销预览';
} elseif ($status == 4) {
return '其他转化';
}
};
foreach ($provider->getModels() as &$item) {
$item['convert_status'] = $covertStatus($item['convert_status']);
$item['telephone'] = ($item['is_virtual'] == 1 ? '【虚拟号码】' : '') . $item['telephone'];
}
return $this->asJson([
'count' => $provider->totalCount,
'code' => 0,
'data' => $provider->models,
'message' => 'ok',
]);
}
public function actionXiansuoPrivate()
{
$localAccountList = ArrayHelper::getColumn(UserAdvertiser::find()->where(['user_id' => Yii::$app->user->id, 'is_delete' => 0])->asArray()->all() ?: [], 'advertiser_id');
// var_dump($localAccountList);die;
$query = Clue::find()->where([
'local_account_id' => $localAccountList,
'convert_status' => 1,
'is_virtual' => 0
])->andFilterWhere(
['like', 'telephone', Yii::$app->request->get('phone')],
)->andFilterWhere(
['like', 'name', Yii::$app->request->get('name')]
)->andFilterWhere(
['like', 'note', Yii::$app->request->get('note')]
)->andFilterWhere(
['like', 'auto_city_name', Yii::$app->request->get('city')]
)->andFilterWhere(
[ '>=', 'create_time_detail', Yii::$app->request->get('date_start')]
)->andFilterWhere(
[ '<=', 'create_time_detail', Yii::$app->request->get('date_end')]
)->orderBy('create_time_detail DESC');
$x = clone $query;
$provider = PaginationHelper::createDataProvider($query, limit: Yii::$app->request->get('limit'));
$covertStatus = function ($status) {
if ($status == 1) {
return '合法转化';
} elseif ($status == 2) {
return '待确认';
} elseif ($status == 3) {
return '营销预览';
} elseif ($status == 4) {
return '其他转化';
}
};
foreach ($provider->getModels() as &$item) {
$item['convert_status'] = $covertStatus($item['convert_status']);
}
return $this->asJson([
'count' => $provider->totalCount,
'code' => 0,
'data' => $provider->models,
'message' => 'ok',
'sql' => $x->createCommand()->getRawSql()
]);
}
/**
*
* @url api/oauth-manage
* @return Response
*/
public function actionOauthManage()
{
$query = Oauth::find();
$provider = PaginationHelper::createDataProvider($query);
foreach ($provider->getModels() as &$item) {
$item['updated_at'] = date('Y-m-d H:i:s', $item['updated_at']);
}
return $this->asJson([
'count' => $provider->totalCount,
'code' => 0,
'data' => $provider->models,
'message' => 'ok',
]);
}
/**
* @url api/oauth-manage-config
*/
public function actionOauthManageConfig()
{
}
/**
* @url api/init-oauth-admin
*/
public function actionInitOauthAdmin()
{
$uid = Yii::$app->request->post('uid');
$tr = Yii::$app->db->beginTransaction();
// Yii::$app->oceanengine->
// 取出授权的account
$oauthAdmin = Oauth::find()->where(['uid' => $uid])->one();
$oauthAdmin->is_init = 1;
$oauthAdmin->save();
// $accounts = json_decode($oauthAdmin->advertiser_ids, true);
// 清理之前所有授权的账户
OauthAccount::updateAll(['admin_uid' => $uid], ['is_delete' => 1]);
$accountList = Yii::$app->oceanengine->getAdminChildAccount($uid);
// 本地推账户
OauthAccountLocal::updateAll(['is_delete' => 1], ['admin_uid' => $uid]);
// 更新account 数据
foreach ($accountList['data']['list'] ?? [] as $account) {
$accountQuery = OauthAccount::find()->where(['account_id' => $account['account_id']])->one() ?: new OauthAccount();
$accountQuery->is_delete = 0;
$accountQuery->admin_uid = $uid;
$accountQuery->account_name = $account['account_name'];
$accountQuery->account_id = strval($account['account_id']);
$accountQuery->save();
$accountLocalList = Yii::$app->oceanengine->getAccountLocal($uid);
foreach ($accountLocalList['data']['list'] ?? [] as $accountLocal) {
$query = OauthAccountLocal::find()->where(['advertiser_id' => $accountLocal['advertiser_id']])->one() ?: new OauthAccountLocal();
$query->is_delete = 0;
$query->admin_uid = $uid;
$query->account_id = strval($account['account_id']);
$query->advertiser_name = strval($accountLocal['advertiser_name']);
$query->advertiser_id = strval($accountLocal['advertiser_id']);
$query->save();
}
}
$tr->commit();
return $this->asJson([
'code' => 0,
'data' => [],
'message' => 'ok',
]);
}
public function actionUpdateClue()
{
$clueId = Yii::$app->request->post('clue_id');
$note = Yii::$app->request->post('note');
$name = Yii::$app->request->post('name');
$convertStatus = Yii::$app->request->post('convert_status');
$query = Clue::find()->where(['clue_id' => $clueId])->one();
$query->note = $note;
$query->name = $name;
if ($convertStatus) {
$query->convert_status = $convertStatus;
}
$query->save();
return $this->asJson([
'code' => 0,
'data' => [],
'message' => 'ok',
]);
}
public function actionOauthAccountLocalUpdate()
{
$advIds = Yii::$app->request->post('advertiser_ids');
if (count($advIds) > 50) {
return $this->asJson([
'code' => -1,
'data' => [],
'message' => '最多只支持50个账户。',
]);
}
$isDelete = [];
foreach ($advIds as $advVal) {
list($id, $accountId) = explode('|', $advVal);
if (!isset($isDelete[$accountId])) {
OauthAccountLocal::updateAll(['is_active' => 0], ['account_id' => $accountId]);
$isDelete[$accountId] = true;
}
$query = OauthAccountLocal::find()->where(['id' => $id])->one();
$query->is_active = 1;
$query->save();
}
return $this->asJson([
'code' => 0,
'data' => [],
'message' => 'ok',
]);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace app\controllers;
use yii\web\Response;
class BaseController extends \yii\web\Controller
{
}

View File

@ -0,0 +1,316 @@
<?php
namespace app\controllers;
use app\models\Clue;
use app\models\Oauth;
use app\models\OauthAccount;
use app\models\OauthAccountLocal;
use app\models\User;
use app\models\UserAdvertiser;
use http\Client;
use Yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\VerbFilter;
use app\models\LoginForm;
use app\models\ContactForm;
use yii\web\UrlManager;
class SiteController extends Controller
{
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'only' => ['logout'],
'rules' => [
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['@'],
],
],
],
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'logout' => ['get'],
],
],
];
}
/**
* {@inheritdoc}
*/
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
public function beforeAction($action)
{
parent::beforeAction($action);
if ($action->id == 'login') {
return true;
}
if (Yii::$app->user->isGuest) {
return $this->redirect('/login');
}
return true;
}
/**
* Displays homepage.
*
* @return string
*/
public function actionIndex()
{
$this->layout = 'main_index';
if (Yii::$app->user->isGuest) {
return $this->redirect(['site/login']);
}
return $this->render('index');
}
/**
* Login action.
*
* @return Response|string
*/
public function actionLogin()
{
if (!Yii::$app->user->isGuest) {
return $this->goHome();
}
$model = new LoginForm();
if ($model->load(Yii::$app->request->post()) && $model->login()) {
return $this->goBack();
}
$model->password = '';
return $this->render('login', [
'model' => $model,
]);
}
/**
* Logout action.
*
* @return Response
*/
public function actionLogout()
{
Yii::$app->user->logout();
return $this->goHome();
}
/**
* Displays contact page.
*
* @return Response|string
*/
public function actionContact()
{
$model = new ContactForm();
if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) {
Yii::$app->session->setFlash('contactFormSubmitted');
return $this->refresh();
}
return $this->render('contact', [
'model' => $model,
]);
}
/**
* Displays about page.
*
* @return string
*/
public function actionAbout()
{
return $this->render('about');
}
public function actionConsole()
{
$this->layout = 'main_index';
return $this->render('console');
}
public function actionXiansuo()
{
$this->layout = 'main_index';
return $this->render('xiansuo');
}
public function actionUsers()
{
$this->layout = 'main_index';
return $this->render('users');
}
public function actionUsercreate()
{
$this->layout = 'main_index';
return $this->render('user/create');
}
public function actionUseredit()
{
$id = Yii::$app->request->get('id');
$this->layout = 'main_index';
$advertiser = UserAdvertiser::find()->where([
'user_id' => $id,
'is_delete' => 0
])->indexBy('advertiser_id')->asArray()->all() ?: [];
$allLocalAccount = OauthAccountLocal::find()->asArray()->all();
$advertiserList = [];
foreach ($allLocalAccount as $item) {
$advertiserList[] = [
'name' => $item['advertiser_name'],
'value' => $item['id'],
'selected' => isset($advertiser[$item['advertiser_id']])
];
}
return $this->render('user/edit', [
'advertiser' => $advertiserList,
'user_id' => $id
]);
}
public function actionUsereditpasswprd()
{
$id = Yii::$app->request->get('id');
$query = User::find()->where(['id' => $id])->asArray()->one();
$this->layout = 'main_index';
return $this->render('user/edit-password', [
'query' => $query
]);
}
public function actionOauth()
{
// http://j56ff926.natappfree.cc/?app_id=1852484891502756&auth_code=e7eb2c40cc7ebe38e359b283701e9406c3f1a382&material_auth_status=1&scope=%5B10000000%2C200000032%2C2%2C3%2C4%2C5%2C300000006%2C300000040%2C300000041%2C130%2C14%2C112%2C300000052%2C110%2C120%2C122%2C123%2C124%2C300000029%2C300000000%2C100000005%5D&state=your_custom_params&uid=4121395460312552
$request = Yii::$app->request->get();
$appId = $request['app_id'];
$authCode = $request['auth_code'];
$materialAuthStatus = $request['material_auth_status'];
$scope = $request['scope'];
$uid = $request['uid'];
$oauth = Oauth::find()->where(['uid' => $uid])->one();
if (!$oauth) {
$oauth = new Oauth();
}
$oauth->app_id = $appId;
$oauth->auth_code = $authCode;
$oauth->material_auth_status = $materialAuthStatus;
$oauth->scope = $scope;
$oauth->uid = $uid;
$oauth->save();
$curl = new \app\common\CurlApp();
$curl->setMethod();
$curl->setUrl('https://ad.oceanengine.com/open_api/oauth2/access_token/');
$curl->setPostData([
"app_id" => Yii::$app->params['app_id'],
"secret" => Yii::$app->params['secret'],
"auth_code" => $authCode
]);
$res = json_decode($curl->exec(), true);
if ($res['code'] != '0') {
throw new \Exception($res['message']);
}
$oauth->advertiser_ids = json_encode($res['data']['advertiser_ids']);
$oauth->access_token = $res['data']['access_token'];
$oauth->refresh_token = $res['data']['refresh_token'];
$oauth->save();
echo '授权成功, 请关闭此页面';die;
}
/**
*
*/
public function actionOauthmanage()
{
// TODO: 管理员验证
$this->layout = 'main_index';
return $this->render('oauth-manage');
}
/**
*
* @url oauthmanageconfig
* @return string
*/
public function actionOauthmanageconfig()
{
// TODO: 管理员验证
$this->layout = 'main_index';
$adminUid = Yii::$app->request->get('uid');
$arr = [];
$accounts = OauthAccount::find()->where(['admin_uid' => $adminUid, 'is_delete' => 0])->all();
foreach ($accounts as $account) {
$arr[$account['account_name']] = [
'id' => $account['account_id'],
'items' => OauthAccountLocal::find()->where(['account_id' => $account['account_id'], 'is_delete' => 0])->asArray()->all()
];
}
return $this->render('oauth-manage-config', [
'arr' => $arr
]);
}
public function actionGenjin()
{
$clueId = Yii::$app->request->get('clue_id');
$note = Clue::find()->where(['clue_id' => $clueId])->one();
$this->layout = 'main_index';
return $this->render('genjin', [
'clueId' => $clueId,
'note' => $note->note,
'name' => $note->name,
'covert_status' => $note->convert_status
]);
}
public function actionPrivate()
{
$this->layout = 'main_index';
return $this->render('xiansuo_private');
}
}

View File

@ -0,0 +1,344 @@
<?php
namespace app\controllers\api;
use app\common\PaginationHelper;
use app\models\Clue;
use app\models\Oauth;
use app\models\OauthAccount;
use app\models\OauthAccountLocal;
use app\models\User;
use app\models\UserAdvertiser;
use app\models\Xiansuo;
use http\Url;
use Yii;
use yii\base\DynamicModel;
use yii\data\Pagination;
use yii\filters\AccessControl;
use yii\helpers\ArrayHelper;
use yii\validators\Validator;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\VerbFilter;
use app\models\LoginForm;
use app\models\ContactForm;
use yii\web\UrlManager;
class UserController extends Controller
{
public function beforeAction($action)
{
parent::beforeAction($action);
if (Yii::$app->user->isGuest) {
return $this->redirect('/login');
}
return true;
}
/**
* {@inheritdoc}
*/
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
public function actionIndex()
{
$query = User::find();
$provider = PaginationHelper::createDataProvider($query);
$models = $provider->getModels();
foreach ($models as &$item) {
$item = $item->toArray();
$item['created_at'] = date('Y-m-d H:i:s', $item['updated_at']);
$localAccountList = ArrayHelper::getColumn(UserAdvertiser::find()->where(['is_delete' => 0, 'user_id' => $item])->asArray()->all(), 'advertiser_name');
$item['advertiser_status'] = implode(',', $localAccountList);
}
return $this->asJson([
'count' => $provider->totalCount,
'code' => 0,
'data' => $models,
'message' => 'ok',
]);
}
public function actionCreate()
{
$model = DynamicModel::validateData(Yii::$app->request->post(), [
// ===== 用户名 =====
[['username'], 'required'],
[['username'], 'trim'],
[['username'], 'string', 'min' => 4, 'max' => 20],
[['username'], 'match', 'pattern' => '/^[a-zA-Z0-9_]+$/', 'message' => '账号只能包含字母、数字和下划线'],
[['username'], 'unique', 'targetClass' => User::class, 'message' => '该账号已存在'],
// ===== 密码 =====
[['password'], 'required'],
[['password'], 'string', 'min' => 6, 'max' => 32],
[['password'], 'match',
'pattern' => '/^(?=.*[A-Za-z])(?=.*\d).+$/',
'message' => '密码必须包含字母和数字'
],
]);
if ($model->hasErrors()) {
return $this->asJson([
'code' => 1,
'msg' => current($model->getFirstErrors())
]);
}
$username = Yii::$app->request->post('username');
$password = Yii::$app->request->post('password');
$model = new User();
$model->username = $username;
$model->auth_key = \Yii::$app->security->generateRandomString();
$model->password_hash = \Yii::$app->security->generatePasswordHash($password);
$model->email = "{$username}.com";
$model->role = 'USER';
$model->created_at = time();
$model->updated_at = time();
$model->save();
// var_dump($model->errors);
return $this->asJson([
'code' => 0,
'message' => 'ok'
]);
}
public function actionUpdate()
{
}
public function actionDelete()
{
}
/**
*
*
* @return Response
*/
public function actionXiansuo()
{
$query = Clue::find()->filterWhere(
['like', 'telephone', Yii::$app->request->get('phone')],
)->andFilterWhere(
['like', 'name', Yii::$app->request->get('name')]
)->andFilterWhere(
['like', 'note', Yii::$app->request->get('note')]
)->orderBy('id DESC');
$provider = PaginationHelper::createDataProvider($query, limit: Yii::$app->request->get('limit'));
return $this->asJson([
'count' => $provider->totalCount,
'code' => 0,
'data' => $provider->models,
'message' => 'ok',
]);
}
/**
*
* @url api/oauth-manage
* @return Response
*/
public function actionOauthManage()
{
$query = Oauth::find();
$provider = PaginationHelper::createDataProvider($query);
foreach ($provider->getModels() as &$item) {
$item['updated_at'] = date('Y-m-d H:i:s', $item['updated_at']);
}
return $this->asJson([
'count' => $provider->totalCount,
'code' => 0,
'data' => $provider->models,
'message' => 'ok',
]);
}
/**
* @url api/oauth-manage-config
*/
public function actionOauthManageConfig()
{
}
/**
* @url api/init-oauth-admin
*/
public function actionInitOauthAdmin()
{
$uid = Yii::$app->request->post('uid');
$tr = Yii::$app->db->beginTransaction();
// Yii::$app->oceanengine->
// 取出授权的account
$oauthAdmin = Oauth::find()->where(['uid' => $uid])->one();
$oauthAdmin->is_init = 1;
$oauthAdmin->save();
// $accounts = json_decode($oauthAdmin->advertiser_ids, true);
// 清理之前所有授权的账户
OauthAccount::updateAll(['admin_uid' => $uid], ['is_delete' => 1]);
$accountList = Yii::$app->oceanengine->getAdminChildAccount($uid);
// 本地推账户
OauthAccountLocal::updateAll(['is_delete' => 1], ['admin_uid' => $uid]);
// 更新account 数据
foreach ($accountList['data']['list'] ?? [] as $account) {
$accountQuery = OauthAccount::find()->where(['account_id' => $account['account_id']])->one() ?: new OauthAccount();
$accountQuery->is_delete = 0;
$accountQuery->admin_uid = $uid;
$accountQuery->account_name = $account['account_name'];
$accountQuery->account_id = strval($account['account_id']);
$accountQuery->save();
$accountLocalList = Yii::$app->oceanengine->getAccountLocal($uid);
foreach ($accountLocalList['data']['list'] ?? [] as $accountLocal) {
$query = OauthAccountLocal::find()->where(['advertiser_id' => $accountLocal['advertiser_id']])->one() ?: new OauthAccountLocal();
$query->is_delete = 0;
$query->admin_uid = $uid;
$query->account_id = strval($account['account_id']);
$query->advertiser_name = strval($accountLocal['advertiser_name']);
$query->advertiser_id = strval($accountLocal['advertiser_id']);
$query->save();
}
}
$tr->commit();
return $this->asJson([
'code' => 0,
'data' => [],
'message' => 'ok',
]);
}
public function actionUpdateClue()
{
$clueId = Yii::$app->request->post('clue_id');
$note = Yii::$app->request->post('note');
$name = Yii::$app->request->post('name');
$query = Clue::find()->where(['clue_id' => $clueId])->one();
$query->note = $note;
$query->name = $name;
$query->save();
return $this->asJson([
'code' => 0,
'data' => [],
'message' => 'ok',
]);
}
public function actionOauthAccountLocalUpdate()
{
$advIds = Yii::$app->request->post('advertiser_ids');
$isDelete = [];
foreach ($advIds as $advVal) {
list($id, $accountId) = explode('|', $advVal);
if (!isset($isDelete[$accountId])) {
OauthAccountLocal::updateAll(['is_active' => 0], ['account_id' => $accountId]);
$isDelete[$accountId] = true;
}
$query = OauthAccountLocal::find()->where(['id' => $id])->one();
$query->is_active = 1;
$query->save();
}
return $this->asJson([
'code' => 0,
'data' => [],
'message' => 'ok',
]);
}
public function actionResetPassword()
{
$password = Yii::$app->request->post('password');
$id = Yii::$app->request->post('id');
$model = User::find()->where(['id' => $id])->one();
$model->auth_key = \Yii::$app->security->generateRandomString();
$model->password_hash = \Yii::$app->security->generatePasswordHash($password);
return $this->asJson([
'code' => 0,
'data' => [],
'message' => $model->save() ? '修改成功' : '修改失败',
]);
}
// public function actionEdit()
// {
// $password = Yii::$app->request->post('password');
// $id = Yii::$app->request->post('id');
// $model = User::find()->where(['id' => $id])->one();
// $model->auth_key = \Yii::$app->security->generateRandomString();
// $model->password_hash = \Yii::$app->security->generatePasswordHash($password);
//
// return $this->asJson([
// 'code' => 0,
// 'data' => [],
// 'message' => $model->save() ? '修改成功' : '修改失败',
// ]);
// }
public function actionUpdateUser()
{
$userId = Yii::$app->request->post('user_id');
$select = Yii::$app->request->post('select', '');
UserAdvertiser::updateAll(['is_delete' => 1], ['user_id' => $userId]);
try {
$select = explode(',', $select);
} catch (\Throwable $exception) {
$select = [];
}
foreach ($select as $item) {
$query = UserAdvertiser::find()->where(['user_id' => $userId, 'local_id' => $item])->one();
if ($query) {
$query->is_delete = 0;
} else {
$model = OauthAccountLocal::find()->where(['id' => $item])->one();
$query = new UserAdvertiser();
$query->advertiser_id = $model->advertiser_id;
$query->user_id = $userId;
$query->local_id = $userId;
$query->advertiser_name = $model->advertiser_name;
}
$query->save();
}
return $this->asJson([
'code' => 0,
'data' => [],
'message' => 'ok',
]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace app\controllers\api\v1;
use app\controllers\BaseController;
use app\models\Brand;
use app\models\BrandRunway;
use yii\web\Controller;
class IndexController extends BaseController
{
public function actionMain()
{
$model = BrandRunway::find()->where(['is_deleted' => 0])->limit(2)->asArray()->all();
foreach ($model as &$item) {
$item['cover'] = 'http://static.23cm.cn/11133613f321ed5eae0dc597f3451cae/37fdd980e520e8632f271730a30e88fc-h480';
$item['brand_name'] = Brand::findOne($item['brand_id'])->name;
}
return $this->asJson([
'code' => 0,
'data' => $model
]);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace app\controllers\api\v1;
use app\common\ImageHelper;
use app\components\serializers\Serializer;
use app\controllers\BaseController;
use app\models\Brand;
use app\models\BrandRunway;
use app\models\BrandRunwayImages;
use yii\helpers\ArrayHelper;
use yii\web\Controller;
class RunwayController extends BaseController
{
public function actionView()
{
$runwayId = \Yii::$app->request->get('id');
$runway = BrandRunway::find()->where(['id' => $runwayId])->asArray()->one();
$runway['brand_name'] = Brand::findOne($runway['brand_id'])->name;
$runwayImages = BrandRunwayImages::find()->where(['runway_id' => $runway['id']])->asArray()->all();
foreach ($runwayImages as &$image) {
$image['s'] = ImageHelper::imageMogr2H480(\Yii::$app->params['cdnAddress'] . $image['name']);
$image['xl'] = ImageHelper::imageMogr2H1080(\Yii::$app->params['cdnAddress'] . $image['name']);
}
return $this->asJson([
'code' => 0,
'data' => [
'info' => $runway,
'images' => $runwayImages
]
]);
}
}

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
version: '2'
services:
php:
image: yiisoftware/yii2-php:7.4-apache
volumes:
- ~/.composer-docker/cache:/root/.composer/cache:delegated
- ./:/app:delegated
ports:
- '8000:80'

22
enums/SourceEnum.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace app\enums;
enum SourceEnum
{
case VOGEU;
public function baseUrl(): string
{
return match($this) {
self::VOGEU => 'https://www.vogue.com/',
};
}
public function value(): int
{
return match($this) {
self::VOGEU => 1,
};
}
}

22
mail/layouts/html.php Normal file
View File

@ -0,0 +1,22 @@
<?php
use yii\helpers\Html;
/** @var \yii\web\View $this view component instance */
/** @var \yii\mail\MessageInterface $message the message being composed */
/** @var string $content main view render result */
?>
<?php $this->beginPage() ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=<?= Yii::$app->charset ?>" />
<title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
<?= $content ?>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

13
mail/layouts/text.php Normal file
View File

@ -0,0 +1,13 @@
<?php
/**
* @var yii\web\View $this view component instance
* @var yii\mail\BaseMessage $message the message being composed
* @var string $content main view render result
*/
$this->beginPage();
$this->beginBody();
echo $content;
$this->endBody();
$this->endPage();

16
models/BaseModel.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace app\models;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
class BaseModel extends ActiveRecord
{
public function behaviors()
{
return [
TimestampBehavior::class,
];
}
}

56
models/Brand.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "brand".
*
* @property int $id
* @property int|null $created_at
* @property int|null $updated_at
* @property string|null $name
* @property string|null $show_name
* @property int|null $is_deleted
*/
class Brand extends BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'brand';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['name', 'show_name'], 'default', 'value' => null],
[['is_deleted'], 'default', 'value' => 0],
[['created_at', 'updated_at', 'is_deleted'], 'integer'],
[['name', 'show_name'], 'string', 'max' => 255],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'name' => 'Name',
'show_name' => 'Show Name',
'is_deleted' => 'Is Deleted',
];
}
}

58
models/BrandAlias.php Normal file
View File

@ -0,0 +1,58 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "brand_alias".
*
* @property int $id
* @property int|null $created_at
* @property int|null $updated_at
* @property string|null $name
* @property int $brand_id
* @property int|null $is_deleted
*/
class BrandAlias extends BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'brand_alias';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['name'], 'default', 'value' => null],
[['is_deleted'], 'default', 'value' => 0],
[['created_at', 'updated_at', 'brand_id', 'is_deleted'], 'integer'],
[['brand_id'], 'required'],
[['name'], 'string', 'max' => 255],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'name' => 'Name',
'brand_id' => 'Brand ID',
'is_deleted' => 'Is Deleted',
];
}
}

68
models/BrandRunway.php Normal file
View File

@ -0,0 +1,68 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "brand_runway".
*
* @property int $id
* @property string|null $title
* @property string|null $description
* @property int|null $created_at
* @property int|null $updated_at
* @property int|null $is_deleted
* @property int|null $image_count
* @property int $brand_id
* @property int|null $year
* @property string|null $cover
* @property string|null $source_url
*/
class BrandRunway extends \app\models\BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'brand_runway';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['source_url'], 'default', 'value' => ''],
[['year'], 'default', 'value' => 0],
[['created_at', 'updated_at', 'is_deleted', 'image_count', 'brand_id', 'year'], 'integer'],
[['title', 'cover', 'source_url'], 'string', 'max' => 255],
[['description'], 'string', 'max' => 1024],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'title' => 'Title',
'description' => 'Description',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'is_deleted' => 'Is Deleted',
'image_count' => 'Image Count',
'brand_id' => 'Brand ID',
'year' => 'Year',
'cover' => 'Cover',
'source_url' => 'Source Url',
];
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "brand_runway_images".
*
* @property int $id
* @property int|null $created_at
* @property int|null $updated_at
* @property int|null $is_deleted
* @property string|null $image
* @property int $runway_id
* @property int $brand_id
* @property string|null $name
*/
class BrandRunwayImages extends \app\models\BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'brand_runway_images';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['brand_id'], 'default', 'value' => 0],
[['name'], 'default', 'value' => ''],
[['created_at', 'updated_at', 'is_deleted', 'runway_id', 'brand_id'], 'integer'],
[['image', 'name'], 'string', 'max' => 255],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'is_deleted' => 'Is Deleted',
'image' => 'Image',
'runway_id' => 'Runway ID',
'brand_id' => 'Brand ID',
'name' => 'Name',
];
}
}

60
models/BrandSource.php Normal file
View File

@ -0,0 +1,60 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "brand_source".
*
* @property int $id
* @property int|null $source
* @property int|null $created_at
* @property int|null $updated_at
* @property int|null $is_deleted
* @property int $brand_id
* @property string|null $source_url
*/
class BrandSource extends \app\models\BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'brand_source';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['created_at', 'updated_at'], 'default', 'value' => null],
[['brand_id'], 'default', 'value' => 0],
[['source_url'], 'default', 'value' => ''],
[['source', 'created_at', 'updated_at', 'is_deleted', 'brand_id'], 'integer'],
[['source_url'], 'string', 'max' => 255],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'source' => 'Source',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'is_deleted' => 'Is Deleted',
'brand_id' => 'Brand ID',
'source_url' => 'Source Url',
];
}
}

150
models/Clue.php Normal file
View File

@ -0,0 +1,150 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "clue".
*
* @property int $id 自增ID
* @property string $clue_id 平台线索ID
* @property string|null $req_id 请求ID
* @property int|null $local_account_id 本地商户ID
* @property string|null $advertiser_name 广告主名称
* @property string|null $promotion_id 推广计划ID
* @property string|null $promotion_name 推广计划名称
* @property string|null $action_type 行为类型
* @property int|null $ad_type 广告类型
* @property string|null $video_id 视频ID
* @property string|null $content_id 内容ID
* @property string|null $title_id 标题ID
* @property string|null $name 姓名
* @property string|null $telephone 手机号
* @property string|null $weixin 微信
* @property string|null $gender 性别
* @property int|null $age 年龄
* @property string|null $province_name
* @property string|null $city_name
* @property string|null $county_name
* @property string|null $address
* @property string|null $auto_province_name 系统识别省
* @property string|null $auto_city_name 系统识别市
* @property string|null $country_name
* @property string|null $author_aweme_id
* @property string|null $author_nickname
* @property string|null $author_role
* @property string|null $staff_aweme_id
* @property string|null $staff_nickname
* @property string|null $allocation_status 分配状态
* @property int|null $convert_status 是否转化
* @property string|null $clue_type 线索类型
* @property string|null $clue_return_status 线索回传状态
* @property int|null $effective_state 是否有效
* @property string|null $effective_state_name
* @property string|null $follow_state_name
* @property string|null $is_private_clue
* @property string|null $remark 备注
* @property string|null $remark_dict 问答/聊天记录
* @property string|null $system_tags 系统标签
* @property string|null $tags 自定义标签
* @property string|null $create_time_detail 线索创建时间
* @property string|null $modify_time 平台更新时间
* @property int|null $created_at
* @property int|null $updated_at
* @property string|null $note
* @property int|null $is_virtual
*/
class Clue extends \app\models\BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'clue';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['req_id', 'local_account_id', 'advertiser_name', 'promotion_id', 'promotion_name', 'action_type', 'ad_type', 'video_id', 'content_id', 'title_id', 'name', 'telephone', 'weixin', 'gender', 'province_name', 'city_name', 'county_name', 'address', 'auto_province_name', 'auto_city_name', 'country_name', 'author_aweme_id', 'author_nickname', 'author_role', 'staff_aweme_id', 'staff_nickname', 'allocation_status', 'clue_type', 'clue_return_status', 'effective_state_name', 'follow_state_name', 'remark', 'remark_dict', 'system_tags', 'tags', 'create_time_detail', 'modify_time', 'note'], 'default', 'value' => null],
[['is_virtual'], 'default', 'value' => 0],
[['is_private_clue'], 'default', 'value' => 'NO'],
[['clue_id'], 'required'],
[['local_account_id', 'ad_type', 'age', 'convert_status', 'effective_state', 'created_at', 'updated_at', 'is_virtual'], 'integer'],
[['remark', 'remark_dict', 'system_tags', 'tags', 'note'], 'string'],
[['create_time_detail', 'modify_time'], 'safe'],
[['clue_id', 'action_type', 'content_id', 'author_aweme_id', 'staff_aweme_id'], 'string', 'max' => 32],
[['req_id'], 'string', 'max' => 64],
[['advertiser_name', 'promotion_id', 'promotion_name', 'video_id', 'title_id', 'is_private_clue'], 'string', 'max' => 100],
[['name', 'weixin', 'province_name', 'city_name', 'county_name', 'auto_province_name', 'auto_city_name', 'country_name', 'author_nickname', 'staff_nickname'], 'string', 'max' => 50],
[['telephone', 'author_role', 'allocation_status', 'clue_type', 'clue_return_status', 'effective_state_name', 'follow_state_name'], 'string', 'max' => 20],
[['gender'], 'string', 'max' => 16],
[['address'], 'string', 'max' => 255],
[['clue_id'], 'unique'],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => '自增ID',
'clue_id' => '平台线索ID',
'req_id' => '请求ID',
'local_account_id' => '本地商户ID',
'advertiser_name' => '广告主名称',
'promotion_id' => '推广计划ID',
'promotion_name' => '推广计划名称',
'action_type' => '行为类型',
'ad_type' => '广告类型',
'video_id' => '视频ID',
'content_id' => '内容ID',
'title_id' => '标题ID',
'name' => '姓名',
'telephone' => '手机号',
'weixin' => '微信',
'gender' => '性别',
'age' => '年龄',
'province_name' => 'Province Name',
'city_name' => 'City Name',
'county_name' => 'County Name',
'address' => 'Address',
'auto_province_name' => '系统识别省',
'auto_city_name' => '系统识别市',
'country_name' => 'Country Name',
'author_aweme_id' => 'Author Aweme ID',
'author_nickname' => 'Author Nickname',
'author_role' => 'Author Role',
'staff_aweme_id' => 'Staff Aweme ID',
'staff_nickname' => 'Staff Nickname',
'allocation_status' => '分配状态',
'convert_status' => '是否转化',
'clue_type' => '线索类型',
'clue_return_status' => '线索回传状态',
'effective_state' => '是否有效',
'effective_state_name' => 'Effective State Name',
'follow_state_name' => 'Follow State Name',
'is_private_clue' => 'Is Private Clue',
'remark' => '备注',
'remark_dict' => '问答/聊天记录',
'system_tags' => '系统标签',
'tags' => '自定义标签',
'create_time_detail' => '线索创建时间',
'modify_time' => '平台更新时间',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'note' => 'Note',
'is_virtual' => 'Is Virtual',
];
}
}

65
models/ContactForm.php Normal file
View File

@ -0,0 +1,65 @@
<?php
namespace app\models;
use Yii;
use yii\base\Model;
/**
* ContactForm is the model behind the contact form.
*/
class ContactForm extends Model
{
public $name;
public $email;
public $subject;
public $body;
public $verifyCode;
/**
* @return array the validation rules.
*/
public function rules()
{
return [
// name, email, subject and body are required
[['name', 'email', 'subject', 'body'], 'required'],
// email has to be a valid email address
['email', 'email'],
// verifyCode needs to be entered correctly
['verifyCode', 'captcha'],
];
}
/**
* @return array customized attribute labels
*/
public function attributeLabels()
{
return [
'verifyCode' => 'Verification Code',
];
}
/**
* Sends an email to the specified email address using the information collected by this model.
* @param string $email the target email address
* @return bool whether the model passes validation
*/
public function contact($email)
{
if ($this->validate()) {
Yii::$app->mailer->compose()
->setTo($email)
->setFrom([Yii::$app->params['senderEmail'] => Yii::$app->params['senderName']])
->setReplyTo([$this->email => $this->name])
->setSubject($this->subject)
->setTextBody($this->body)
->send();
return true;
}
return false;
}
}

81
models/LoginForm.php Normal file
View File

@ -0,0 +1,81 @@
<?php
namespace app\models;
use Yii;
use yii\base\Model;
/**
* LoginForm is the model behind the login form.
*
* @property-read User|null $user
*
*/
class LoginForm extends Model
{
public $username;
public $password;
public $rememberMe = true;
private $_user = false;
/**
* @return array the validation rules.
*/
public function rules()
{
return [
// username and password are both required
[['username', 'password'], 'required'],
// rememberMe must be a boolean value
['rememberMe', 'boolean'],
// password is validated by validatePassword()
['password', 'validatePassword'],
];
}
/**
* Validates the password.
* This method serves as the inline validation for password.
*
* @param string $attribute the attribute currently being validated
* @param array $params the additional name-value pairs given in the rule
*/
public function validatePassword($attribute, $params)
{
if (!$this->hasErrors()) {
$user = $this->getUser();
if (!$user || !$user->validatePassword($this->password)) {
$this->addError($attribute, 'Incorrect username or password.');
}
}
}
/**
* Logs in a user using the provided username and password.
* @return bool whether the user is logged in successfully
*/
public function login()
{
if ($this->validate()) {
return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0);
}
return false;
}
/**
* Finds user by [[username]]
*
* @return User|null
*/
public function getUser()
{
if ($this->_user === false) {
$this->_user = User::findByUsername($this->username);
}
return $this->_user;
}
}

74
models/Oauth.php Normal file
View File

@ -0,0 +1,74 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "oauth".
*
* @property int $id
* @property string $access_token
* @property string $refresh_token
* @property int $created_at
* @property int $updated_at
* @property string $app_id
* @property string $auth_code
* @property string $material_auth_status
* @property string $scope
* @property string $uid
* @property int $is_init
* @property int|null $is_delete
* @property string|null $advertiser_ids
*/
class Oauth extends \app\models\BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'oauth';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['advertiser_ids'], 'default', 'value' => null],
[['uid'], 'default', 'value' => ''],
[['is_delete'], 'default', 'value' => 0],
[['created_at', 'updated_at', 'is_init', 'is_delete'], 'integer'],
[['advertiser_ids'], 'string'],
[['access_token', 'refresh_token', 'auth_code', 'material_auth_status', 'scope', 'uid'], 'string', 'max' => 255],
[['app_id'], 'string', 'max' => 64],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'access_token' => 'Access Token',
'refresh_token' => 'Refresh Token',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'app_id' => 'App ID',
'auth_code' => 'Auth Code',
'material_auth_status' => 'Material Auth Status',
'scope' => 'Scope',
'uid' => 'Uid',
'is_init' => 'Is Init',
'is_delete' => 'Is Delete',
'advertiser_ids' => 'Advertiser Ids',
];
}
}

60
models/OauthAccount.php Normal file
View File

@ -0,0 +1,60 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "oauth_account".
*
* @property int $id
* @property int|null $created_at
* @property int|null $updated_at
* @property string $admin_uid
* @property string $account_id
* @property string $account_name
* @property int|null $is_delete
*/
class OauthAccount extends \app\models\BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'oauth_account';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['is_delete'], 'default', 'value' => 0],
[['account_name'], 'default', 'value' => ''],
[['created_at', 'updated_at', 'is_delete'], 'integer'],
[['admin_uid', 'account_id'], 'string', 'max' => 64],
[['account_name'], 'string', 'max' => 255],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'admin_uid' => 'Admin Uid',
'account_id' => 'Account ID',
'account_name' => 'Account Name',
'is_delete' => 'Is Delete',
];
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "oauth_account_local".
*
* @property int $id
* @property int|null $created_at
* @property int|null $updated_at
* @property string $admin_uid
* @property string $account_id
* @property string $advertiser_name
* @property string $advertiser_id
* @property int|null $is_delete
* @property int $is_active
*/
class OauthAccountLocal extends \app\models\BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'oauth_account_local';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['is_active'], 'default', 'value' => 0],
[['advertiser_id'], 'default', 'value' => ''],
[['created_at', 'updated_at', 'is_delete', 'is_active'], 'integer'],
[['admin_uid', 'account_id', 'advertiser_id'], 'string', 'max' => 64],
[['advertiser_name'], 'string', 'max' => 255],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'admin_uid' => 'Admin Uid',
'account_id' => 'Account ID',
'advertiser_name' => 'Advertiser Name',
'advertiser_id' => 'Advertiser ID',
'is_delete' => 'Is Delete',
'is_active' => 'Is Active',
];
}
}

158
models/User.php Normal file
View File

@ -0,0 +1,158 @@
<?php
namespace app\models;
use Yii;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\BaseActiveRecord;
use yii\web\IdentityInterface;
/**
* This is the model class for table "user".
*
* @property int $id
* @property string $username
* @property string $auth_key
* @property string $password_hash
* @property string|null $password_reset_token
* @property string $email
* @property int $status
* @property int $created_at
* @property int $updated_at
* @property string $role
*/
class User extends BaseModel implements IdentityInterface
{
const STATUS_DELETED = 0;
const STATUS_ACTIVE = 10;
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'user';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['password_reset_token'], 'default', 'value' => null],
[['status'], 'default', 'value' => 10],
[['username', 'auth_key', 'password_hash', 'email', 'created_at', 'updated_at'], 'required'],
[['status', 'created_at', 'updated_at'], 'integer'],
[['username', 'password_hash', 'password_reset_token', 'email'], 'string', 'max' => 255],
[['auth_key', 'role'], 'string', 'max' => 32],
[['username'], 'unique'],
[['email'], 'unique'],
[['password_reset_token'], 'unique'],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'username' => 'Username',
'auth_key' => 'Auth Key',
'password_hash' => 'Password Hash',
'password_reset_token' => 'Password Reset Token',
'email' => 'Email',
'status' => 'Status',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'role' => 'Role',
];
}
/**
* {@inheritdoc}
*/
public static function findIdentity($id)
{
return static::find()
->where(['id' => $id])
->andWhere(['status' => self::STATUS_ACTIVE])
->one();
}
/**
* {@inheritdoc}
*/
public static function findIdentityByAccessToken($token, $type = null)
{
die(__FILE__);
foreach (self::$users as $user) {
if ($user['accessToken'] === $token) {
return new static($user);
}
}
return null;
}
/**
* Finds user by username
*
* @param string $username
* @return static|null
*/
public static function findByUsername($username)
{
return static::find()
->where(['username' => $username])
->andWhere(['status' => self::STATUS_ACTIVE])
->one();
}
/**
* {@inheritdoc}
*/
public function getId()
{
return $this->id;
}
/**
* {@inheritdoc}
*/
public function getAuthKey()
{
return $this->auth_key;
}
/**
* {@inheritdoc}
*/
public function validateAuthKey($authKey)
{
return $this->auth_key === $authKey;
}
/**
* Validates password
*
* @param string $password password to validate
* @return bool if password provided is valid for current user
*/
public function validatePassword($password)
{
return Yii::$app->security->validatePassword(
$password,
$this->password_hash
);
}
public function getRole()
{
return $this->role;
}
}

63
models/UserAdvertiser.php Normal file
View File

@ -0,0 +1,63 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "user_advertiser".
*
* @property int $id
* @property int $user_id
* @property string $advertiser_id
* @property int|null $created_at
* @property int|null $updated_at
* @property int|null $is_delete
* @property int $local_id
* @property string $advertiser_name
*/
class UserAdvertiser extends \app\models\BaseModel
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'user_advertiser';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['is_delete'], 'default', 'value' => 0],
[['advertiser_id'], 'default', 'value' => ''],
[['user_id', 'created_at', 'updated_at', 'is_delete', 'local_id'], 'integer'],
[['local_id', 'advertiser_name'], 'required'],
[['advertiser_id'], 'string', 'max' => 255],
[['advertiser_name'], 'string', 'max' => 64],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'user_id' => 'User ID',
'advertiser_id' => 'Advertiser ID',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
'is_delete' => 'Is Delete',
'local_id' => 'Local ID',
'advertiser_name' => 'Advertiser Name',
];
}
}

104
models/User_bak.php Normal file
View File

@ -0,0 +1,104 @@
<?php
namespace app\models;
class User_bak extends \yii\base\BaseObject implements \yii\web\IdentityInterface
{
public $id;
public $username;
public $password;
public $authKey;
public $accessToken;
private static $users = [
'100' => [
'id' => '100',
'username' => 'admin',
'password' => 'admin',
'authKey' => 'test100key',
'accessToken' => '100-token',
],
'101' => [
'id' => '101',
'username' => 'demo',
'password' => 'demo',
'authKey' => 'test101key',
'accessToken' => '101-token',
],
];
/**
* {@inheritdoc}
*/
public static function findIdentity($id)
{
return isset(self::$users[$id]) ? new static(self::$users[$id]) : null;
}
/**
* {@inheritdoc}
*/
public static function findIdentityByAccessToken($token, $type = null)
{
foreach (self::$users as $user) {
if ($user['accessToken'] === $token) {
return new static($user);
}
}
return null;
}
/**
* Finds user by username
*
* @param string $username
* @return static|null
*/
public static function findByUsername($username)
{
foreach (self::$users as $user) {
if (strcasecmp($user['username'], $username) === 0) {
return new static($user);
}
}
return null;
}
/**
* {@inheritdoc}
*/
public function getId()
{
return $this->id;
}
/**
* {@inheritdoc}
*/
public function getAuthKey()
{
return $this->authKey;
}
/**
* {@inheritdoc}
*/
public function validateAuthKey($authKey)
{
return $this->authKey === $authKey;
}
/**
* Validates password
*
* @param string $password password to validate
* @return bool if password provided is valid for current user
*/
public function validatePassword($password)
{
return $this->password === $password;
}
}

113
models/Xiansuo.php Normal file
View File

@ -0,0 +1,113 @@
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "xiansuo".
*
* @property int $id 主键ID
* @property int|null $xs_id 线索ID业务ID
* @property int|null $leads_id 广告平台线索ID
* @property string|null $name 姓名
* @property string|null $telphone 电话
* @property string|null $weixin 微信
* @property string|null $gender 性别
* @property string|null $location 所在地
* @property string|null $tel_logic_location 电话逻辑归属地
* @property string|null $keshi 科室
* @property string|null $gj_value 跟进状态
* @property string|null $beizhu 备注
* @property string|null $adv_id 广告账户ID
* @property string|null $adv_name 广告账户名称
* @property int|null $promotion_id 计划ID
* @property string|null $promotion_name 计划名
* @property string|null $app_name 来源平台
* @property string|null $root_adv_id 根账户ID
* @property string|null $external_url 外部URL
* @property string|null $clue_convert_status 转化状态
* @property int|null $user_id 分配用户ID
* @property string|null $user_name 分配用户名
* @property int|null $is_daili 是否代理 0否 1是
* @property string|null $remark_dict 问答/聊天记录
* @property string|null $content 原始内容JSON
* @property string|null $push_return_data 推送返回数据
* @property string|null $date 分配时间
* @property string|null $created_at 创建时间
* @property string|null $updated_at 更新时间
*/
class Xiansuo extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'xiansuo';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['xs_id', 'promotion_id', 'user_id', 'remark_dict', 'content', 'push_return_data', 'date', 'created_at', 'updated_at'], 'default', 'value' => null],
[['is_daili'], 'default', 'value' => 0],
[['user_name'], 'default', 'value' => ''],
[['id'], 'required'],
[['id', 'xs_id', 'leads_id', 'promotion_id', 'user_id', 'is_daili'], 'integer'],
[['remark_dict', 'date', 'created_at', 'updated_at'], 'safe'],
[['content', 'push_return_data'], 'string'],
[['name', 'weixin', 'keshi', 'gj_value', 'adv_id', 'app_name', 'root_adv_id', 'clue_convert_status', 'user_name'], 'string', 'max' => 50],
[['telphone'], 'string', 'max' => 20],
[['gender'], 'string', 'max' => 10],
[['location', 'tel_logic_location', 'adv_name'], 'string', 'max' => 100],
[['beizhu', 'external_url'], 'string', 'max' => 255],
[['promotion_name'], 'string', 'max' => 150],
[['id'], 'unique'],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => '主键ID',
'xs_id' => '线索ID业务ID',
'leads_id' => '广告平台线索ID',
'name' => '姓名',
'telphone' => '电话',
'weixin' => '微信',
'gender' => '性别',
'location' => '所在地',
'tel_logic_location' => '电话逻辑归属地',
'keshi' => '科室',
'gj_value' => '跟进状态',
'beizhu' => '备注',
'adv_id' => '广告账户ID',
'adv_name' => '广告账户名称',
'promotion_id' => '计划ID',
'promotion_name' => '计划名',
'app_name' => '来源平台',
'root_adv_id' => '根账户ID',
'external_url' => '外部URL',
'clue_convert_status' => '转化状态',
'user_id' => '分配用户ID',
'user_name' => '分配用户名',
'is_daili' => '是否代理 0否 1是',
'remark_dict' => '问答/聊天记录',
'content' => '原始内容JSON',
'push_return_data' => '推送返回数据',
'date' => '分配时间',
'created_at' => '创建时间',
'updated_at' => '更新时间',
];
}
}

93
models/jobs/RunwayJob.php Normal file
View File

@ -0,0 +1,93 @@
<?php
namespace app\models\jobs;
use app\common\CommonHelper;
use app\enums\SourceEnum;
use app\models\BrandRunway;
use app\models\BrandRunwayImages;
use app\models\BrandSource;
use app\models\jobs\base\BaseJob;
use app\models\logics\commands\SpiderVogue;
use yii\debug\models\search\Db;
use yii\queue\Queue;
class RunwayJob extends BaseJob
{
public BrandSource|null $source = null;
public function execute($queue)
{
$spiderModel = new SpiderVogue();
$runwayList = ($spiderModel->getBrandRunwayList(rtrim(SourceEnum::VOGEU->baseUrl(), '/') . $this->source->source_url));
echo '获取到了 --> ' . count($runwayList ?: []) . '篇文章' . PHP_EOL;
try {
foreach ($runwayList as $runwayItem) {
$tr = \Yii::$app->db->beginTransaction();
echo "正在处理{$runwayItem['hed']}" . PHP_EOL;
if (!$model = BrandRunway::find()->where(['brand_id' => $this->source->brand_id, 'title' => $runwayItem['hed']])->one()) {
$model = new BrandRunway();
$model->title = $runwayItem['hed'];
$model->brand_id = $this->source->brand_id;
$model->year = CommonHelper::getYear($runwayItem['hed']);
$model->cover = '';
$model->save();
var_dump($model->errors);
if (!$sourceModel = BrandSource::find()->where(['brand_id' => $this->source->brand_id, 'is_deleted' => 0])->one()) {
$sourceModel = new BrandSource();
$sourceModel->brand_id = $this->source->brand_id;
$sourceModel->source = SourceEnum::VOGEU->value();
$sourceModel->save();
var_dump($sourceModel->errors);
}
$pageUri = $runwayItem['url'];
$requestUrl = rtrim(SourceEnum::VOGEU->baseUrl(), '/') . $pageUri . '/slideshow/collection';
$detailData = $spiderModel->getDetail($requestUrl);
preg_match_all('/window\.__PRELOADED_STATE__ = (.*?);</s', $detailData, $matches);
$saveUrl = $detailUrl = [];
if (count($matches) > 1) {
$val = json_decode(current($matches[1]), true);
$images = $val['transformed']['runwayGalleries']['galleries'][0]['items'] ?? false;
if ($images === false) {
echo '获取图片失败' . PHP_EOL;
// $this->logger->warning($requestUrl . '获取图片失败.');
$tr->rollBack();
continue;
// return;
}
echo "文章图片数量" . count($images) . PHP_EOL;
foreach (is_array($images) ? $images : [] as $img) {
$saveUrl[] = [
'src' => $img['image']['sources']['xxl']['url']
];
// foreach ($img['details'] ?? [] as $detail) {
// $detailUrl[] = ['src' => $detail['image']['sources']['xxl']['url']];
$brandModel = new BrandRunwayImages();
$brandModel->image = $img['image']['sources']['xxl']['url'];
$brandModel->runway_id = $model->id;
$brandModel->brand_id = $this->source->brand_id;
$brandModel->save();
var_dump($brandModel->errors);
// }
}
// $model->images = json_encode($saveUrl);
$model->image_count = count($saveUrl);
$model->cover = current($saveUrl)['src'];
$model->source_url = $requestUrl;
$model->save();
}
}
$tr->commit();
}
}catch (\Throwable $exception) {
var_dump($exception->getMessage());
}
// TODO: Implement execute() method.
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace app\models\jobs\base;
use yii\queue\Queue;
class BaseJob extends \yii\base\BaseObject implements \yii\queue\JobInterface
{
public function execute($queue)
{
// TODO: Implement execute() method.
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace app\models\logics\commands\base;
use app\common\CurlApp;
class BaseCommandSpiderLogic
{
protected ?CurlApp $curl = null;
protected string $brandName = '';
protected string $baseUrl = '';
public function __construct()
{
ini_set('pcre.backtrack_limit', '-1');
$this->curl = new CurlApp();
}
public function exec()
{
$url = rtrim($this->baseUrl, '/') . '/fashion-shows/designer/' . $this->brandName;
// var_dump($url);
// list($request, $httpCode) = $curl->setUrl($url)->exec();
$request = $this->curl->setUrl($url)->exec();
if ($request) {
preg_match_all('/window.__PRELOADED_STATE__ = ([\s\S]*?);<\/script>/', $request, $matches);
$val = json_decode(current(end($matches)), true);
return $val['transformed']['runwayDesignerContent']['designerCollections'] ?? [];
} else {
// $this->logger->info('未找到数据.');
return [];
}
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace app\models\logics\commands;
use app\common\CommonHelper;
use app\enums\SourceEnum;
use app\models\Brand;
use app\models\BrandRunway;
use app\models\BrandRunwayImages;
use app\models\BrandSource;
use app\models\logics\commands\base\BaseCommandSpiderLogic;
use yii\helpers\ArrayHelper;
class SpiderVogue extends BaseCommandSpiderLogic
{
protected string $baseUrl = 'https://www.vogue.com/';
public function __construct()
{
parent::__construct();
// $this->curl->setProxy('127.0.0.1', 58591);
}
public function setBrandName(string $name = ''): SpiderVogue
{
$this->brandName = $name;
return $this;
}
public function start()
{
$source = BrandSource::find()->where(['source' => SourceEnum::VOGEU->value()])->asArray()->all();
// var_dump(ArrayHelper::getColumn($source, 'brand_id'));
$i = 0;
$brandNameContainer = [];
$brandIdContainer = [];
foreach (Brand::find()->where(
['>','id', 5747] // 5897
)->each() as $item) {
$item['name'] = strtr($item['name'], [
' ' => '-',
'.' => '-'
]);
// var_dump(strtolower($item['name']));
// $runwayList = $this->setBrandName($item['name'])->_getBrandRunwayList(strtolower($item['name']));
echo $item['name'] . PHP_EOL;
$brandNameContainer[] = strtolower($item['name']);
$brandIdContainer[] = $item['id'];
$i++;
if ($i != 5) {
continue;
}
$runwayList = $this->_getBrandRunwayListMulti($brandNameContainer);
foreach ($runwayList as $index => $listItem) {
// var_dump('id---', $brandIdContainer[$index]);
// var_dump($listItem);
if (!$listItem) {
continue;
}
if (!$sourceModel = BrandSource::find()->where(['brand_id' => $brandIdContainer[$index], 'is_deleted' => 0])->one()) {
$sourceModel = new BrandSource();
$sourceModel->brand_id = $brandIdContainer[$index];
$sourceModel->source = SourceEnum::VOGEU->value();
$sourceModel->save();
var_dump($sourceModel->errors);
}
}
$brandNameContainer = $brandIdContainer = [];
$i = 0;
if (!$runwayList) {
continue;
}
var_dump($runwayList);
foreach ($runwayList as $runwayItem) {
var_dump($runwayItem['hed']);
if (!$model = BrandRunway::find()->where(['brand_id' => $item['id'], 'title' => $runwayItem['hed']])->one()) {
echo $runwayItem['hed'] . PHP_EOL;
$model = new BrandRunway();
$model->title = $runwayItem['hed'];
$model->brand_id = $item['id'];
$model->year = CommonHelper::getYear($runwayItem['hed']);
$model->cover = '';
$model->save();
var_dump($model->errors);
if (!$sourceModel = BrandSource::find()->where(['brand_id' => $item['id'], 'is_deleted' => 0])->one()) {
$sourceModel = new BrandSource();
$sourceModel->brand_id = $item['id'];
$sourceModel->source = SourceEnum::VOGEU->value();
$sourceModel->save();
var_dump($sourceModel->errors);
}
$pageUri = $runwayItem['url'];
$requestUrl = rtrim($this->baseUrl, '/') . $pageUri . '/slideshow/collection';
$detailData = $this->_getDetail($requestUrl);
preg_match_all('/window\.__PRELOADED_STATE__ = (.*?);</s', $detailData, $matches);
$saveUrl = $detailUrl = [];
if (count($matches) > 1) {
$val = json_decode(current($matches[1]), true);
$images = $val['transformed']['runwayGalleries']['galleries'][0]['items'] ?? false;
if ($images === false) {
echo '获取图片失败' . PHP_EOL;
// $this->logger->warning($requestUrl . '获取图片失败.');
return;
}
foreach (is_array($images) ? $images : [] as $img) {
$saveUrl[] = [
'src' => $img['image']['sources']['xxl']['url']
];
foreach ($img['details'] ?? [] as $detail) {
$detailUrl[] = ['src' => $detail['image']['sources']['xxl']['url']];
$brandModel = new BrandRunwayImages();
$brandModel->image = $detail['image']['sources']['xxl']['url'];
$brandModel->runway_id = $model->id;
$brandModel->brand_id = $item['id'];
$brandModel->save();
var_dump($brandModel->errors);
}
}
// $model->images = json_encode($saveUrl);
$model->cover = '';
$model->save();
}
}
}
}
}
public function getDetail($url): bool|string
{
return $this->curl->setUrl($url)->exec();
}
public function getBrandRunwayList(string $url)
{
$request = $this->curl->setUrl($url)->exec();
if ($request) {
preg_match_all('/window.__PRELOADED_STATE__ = ([\s\S]*?);<\/script>/', $request, $matches);
$val = json_decode(current(end($matches)), true);
return $val['transformed']['runwayDesignerContent']['designerCollections'] ?? [];
} else {
// $this->logger->info('未找到数据.');
return [];
}
}
private function _getBrandRunwayListMulti(array $brandNames = []): array
{
// $brandNames = [];
$resp = [];
foreach ($brandNames as $brandName) {
$url[] = rtrim($this->baseUrl, '/') . '/fashion-shows/designer/' . $brandName;
// var_dump($url);
// list($request, $httpCode) = $curl->setUrl($url)->exec();
}
$request = $this->curl->execMulti($url);
// var_dump($request);die;
foreach ($request as $item) {
if ($item) {
preg_match_all('/window.__PRELOADED_STATE__ = ([\s\S]*?);<\/script>/', $item, $matches);
$val = json_decode(current(end($matches)), true);
$resp[] = $val['transformed']['runwayDesignerContent']['designerCollections'] ?? [];
} else {
// $this->logger->info('未找到数据.');
$resp[] = [];
}
}
return $resp;
}
}

162
requirements.php Normal file
View File

@ -0,0 +1,162 @@
<?php
/**
* Application requirement checker script.
*
* In order to run this script use the following console command:
* php requirements.php
*
* In order to run this script from the web, you should copy it to the web root.
* If you are using Linux you can create a hard link instead, using the following command:
* ln ../requirements.php requirements.php
*/
// you may need to adjust this path to the correct Yii framework path
// uncomment and adjust the following line if Yii is not located at the default path
//$frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2';
if (!isset($frameworkPath)) {
$searchPaths = array(
dirname(__FILE__) . '/vendor/yiisoft/yii2',
dirname(__FILE__) . '/../vendor/yiisoft/yii2',
);
foreach ($searchPaths as $path) {
if (is_dir($path)) {
$frameworkPath = $path;
break;
}
}
}
if (!isset($frameworkPath) || !is_dir($frameworkPath)) {
$message = "<h1>Error</h1>\n\n"
. "<p><strong>The path to yii framework seems to be incorrect.</strong></p>\n"
. '<p>You need to install Yii framework via composer or adjust the framework path in file <abbr title="' . __FILE__ . '">' . basename(__FILE__) . "</abbr>.</p>\n"
. '<p>Please refer to the <abbr title="' . dirname(__FILE__) . "/README.md\">README</abbr> on how to install Yii.</p>\n";
if (!empty($_SERVER['argv'])) {
// do not print HTML when used in console mode
echo strip_tags($message);
} else {
echo $message;
}
exit(1);
}
require_once($frameworkPath . '/requirements/YiiRequirementChecker.php');
$requirementsChecker = new YiiRequirementChecker();
$gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.';
$gdOK = $imagickOK = false;
if (extension_loaded('imagick')) {
$imagick = new Imagick();
$imagickFormats = $imagick->queryFormats('PNG');
if (in_array('PNG', $imagickFormats)) {
$imagickOK = true;
} else {
$imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.';
}
}
if (extension_loaded('gd')) {
$gdInfo = gd_info();
if (!empty($gdInfo['FreeType Support'])) {
$gdOK = true;
} else {
$gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.';
}
}
/**
* Adjust requirements according to your application specifics.
*/
$requirements = array(
// Database :
array(
'name' => 'PDO extension',
'mandatory' => true,
'condition' => extension_loaded('pdo'),
'by' => 'All DB-related classes',
),
array(
'name' => 'PDO SQLite extension',
'mandatory' => false,
'condition' => extension_loaded('pdo_sqlite'),
'by' => 'All DB-related classes',
'memo' => 'Required for SQLite database.',
),
array(
'name' => 'PDO MySQL extension',
'mandatory' => false,
'condition' => extension_loaded('pdo_mysql'),
'by' => 'All DB-related classes',
'memo' => 'Required for MySQL database.',
),
array(
'name' => 'PDO PostgreSQL extension',
'mandatory' => false,
'condition' => extension_loaded('pdo_pgsql'),
'by' => 'All DB-related classes',
'memo' => 'Required for PostgreSQL database.',
),
// Cache :
array(
'name' => 'Memcache extension',
'mandatory' => false,
'condition' => extension_loaded('memcache') || extension_loaded('memcached'),
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-caching-memcache.html">MemCache</a>',
'memo' => extension_loaded('memcached') ? 'To use memcached set <a href="https://www.yiiframework.com/doc-2.0/yii-caching-memcache.html#$useMemcached-detail">MemCache::useMemcached</a> to <code>true</code>.' : ''
),
// CAPTCHA:
array(
'name' => 'GD PHP extension with FreeType support',
'mandatory' => false,
'condition' => $gdOK,
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
'memo' => $gdMemo,
),
array(
'name' => 'ImageMagick PHP extension with PNG support',
'mandatory' => false,
'condition' => $imagickOK,
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
'memo' => $imagickMemo,
),
// PHP ini :
'phpExposePhp' => array(
'name' => 'Expose PHP',
'mandatory' => false,
'condition' => $requirementsChecker->checkPhpIniOff("expose_php"),
'by' => 'Security reasons',
'memo' => '"expose_php" should be disabled at php.ini',
),
'phpAllowUrlInclude' => array(
'name' => 'PHP allow url include',
'mandatory' => false,
'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"),
'by' => 'Security reasons',
'memo' => '"allow_url_include" should be disabled at php.ini',
),
'phpSmtp' => array(
'name' => 'PHP mail SMTP',
'mandatory' => false,
'condition' => strlen(ini_get('SMTP')) > 0,
'by' => 'Email sending',
'memo' => 'PHP mail SMTP server required',
),
);
// OPcache check
if (!version_compare(phpversion(), '5.5', '>=')) {
$requirements[] = array(
'name' => 'APC extension',
'mandatory' => false,
'condition' => extension_loaded('apc'),
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-caching-apccache.html">ApcCache</a>',
);
}
$result = $requirementsChecker->checkYii()->check($requirements)->getResult();
$requirementsChecker->render();
exit($result['summary']['errors'] === 0 ? 0 : 1);

2
runtime/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

BIN
tests/.DS_Store vendored Normal file

Binary file not shown.

6
tests/_bootstrap.php Normal file
View File

@ -0,0 +1,6 @@
<?php
define('YII_ENV', 'test');
defined('YII_DEBUG') or define('YII_DEBUG', true);
require_once __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
require __DIR__ .'/../vendor/autoload.php';

1
tests/_data/.gitkeep Normal file
View File

@ -0,0 +1 @@

2
tests/_output/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,26 @@
<?php
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*/
class AcceptanceTester extends \Codeception\Actor
{
use _generated\AcceptanceTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*/
class FunctionalTester extends \Codeception\Actor
{
use _generated\FunctionalTesterActions;
}

View File

@ -0,0 +1,26 @@
<?php
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*/
class UnitTester extends \Codeception\Actor
{
use _generated\UnitTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -0,0 +1,10 @@
actor: AcceptanceTester
modules:
enabled:
- WebDriver:
url: http://127.0.0.1:8080/
browser: firefox
- Yii2:
part: orm
entryScript: index-test.php
cleanup: false

View File

@ -0,0 +1,12 @@
<?php
use yii\helpers\Url;
class AboutCest
{
public function ensureThatAboutWorks(AcceptanceTester $I)
{
$I->amOnPage(Url::toRoute('/site/about'));
$I->see('About', 'h1');
}
}

View File

@ -0,0 +1,34 @@
<?php
use yii\helpers\Url;
class ContactCest
{
public function _before(\AcceptanceTester $I)
{
$I->amOnPage(Url::toRoute('/site/contact'));
}
public function contactPageWorks(AcceptanceTester $I)
{
$I->wantTo('ensure that contact page works');
$I->see('Contact', 'h1');
}
public function contactFormCanBeSubmitted(AcceptanceTester $I)
{
$I->amGoingTo('submit contact form with correct data');
$I->fillField('#contactform-name', 'tester');
$I->fillField('#contactform-email', 'tester@example.com');
$I->fillField('#contactform-subject', 'test subject');
$I->fillField('#contactform-body', 'test content');
$I->fillField('#contactform-verifycode', 'testme');
$I->click('contact-button');
$I->wait(2); // wait for button to be clicked
$I->dontSeeElement('#contact-form');
$I->see('Thank you for contacting us. We will respond to you as soon as possible.');
}
}

View File

@ -0,0 +1,18 @@
<?php
use yii\helpers\Url;
class HomeCest
{
public function ensureThatHomePageWorks(AcceptanceTester $I)
{
$I->amOnPage(Url::toRoute('/site/index'));
$I->see('My Company');
$I->seeLink('About');
$I->click('About');
$I->wait(2); // wait for page to be opened
$I->see('This is the About page.');
}
}

View File

@ -0,0 +1,21 @@
<?php
use yii\helpers\Url;
class LoginCest
{
public function ensureThatLoginWorks(AcceptanceTester $I)
{
$I->amOnPage(Url::toRoute('/site/login'));
$I->see('Login', 'h1');
$I->amGoingTo('try to login with correct credentials');
$I->fillField('input[name="LoginForm[username]"]', 'admin');
$I->fillField('input[name="LoginForm[password]"]', 'admin');
$I->click('login-button');
$I->wait(2); // wait for button to be clicked
$I->expectTo('see user info');
$I->see('Logout');
}
}

View File

@ -0,0 +1 @@
<?php

29
tests/bin/yii Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env php
<?php
/**
* Yii console bootstrap file.
*
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test');
require __DIR__ . '/../../vendor/autoload.php';
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
$config = yii\helpers\ArrayHelper::merge(
require __DIR__ . '/../../config/console.php',
[
'components' => [
'db' => require __DIR__ . '/../../config/test_db.php'
]
]
);
$application = new yii\console\Application($config);
$exitCode = $application->run();
exit($exitCode);

20
tests/bin/yii.bat Normal file
View File

@ -0,0 +1,20 @@
@echo off
rem -------------------------------------------------------------
rem Yii command line bootstrap script for Windows.
rem
rem @author Qiang Xue <qiang.xue@gmail.com>
rem @link https://www.yiiframework.com/
rem @copyright Copyright (c) 2008 Yii Software LLC
rem @license https://www.yiiframework.com/license/
rem -------------------------------------------------------------
@setlocal
set YII_PATH=%~dp0
if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
"%PHP_COMMAND%" "%YII_PATH%yii" %*
@endlocal

View File

@ -0,0 +1,14 @@
# Codeception Test Suite Configuration
# suite for functional (integration) tests.
# emulate web requests and make application process them.
# (tip: better to use with frameworks).
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
#basic/web/index.php
actor: FunctionalTester
modules:
enabled:
- Filesystem
- Yii2
- Asserts

View File

@ -0,0 +1,57 @@
<?php
class ContactFormCest
{
public function _before(\FunctionalTester $I)
{
$I->amOnRoute('site/contact');
}
public function openContactPage(\FunctionalTester $I)
{
$I->see('Contact', 'h1');
}
public function submitEmptyForm(\FunctionalTester $I)
{
$I->submitForm('#contact-form', []);
$I->expectTo('see validations errors');
$I->see('Contact', 'h1');
$I->see('Name cannot be blank');
$I->see('Email cannot be blank');
$I->see('Subject cannot be blank');
$I->see('Body cannot be blank');
$I->see('The verification code is incorrect');
}
public function submitFormWithIncorrectEmail(\FunctionalTester $I)
{
$I->submitForm('#contact-form', [
'ContactForm[name]' => 'tester',
'ContactForm[email]' => 'tester.email',
'ContactForm[subject]' => 'test subject',
'ContactForm[body]' => 'test content',
'ContactForm[verifyCode]' => 'testme',
]);
$I->expectTo('see that email address is wrong');
$I->dontSee('Name cannot be blank', '.help-inline');
$I->see('Email is not a valid email address.');
$I->dontSee('Subject cannot be blank', '.help-inline');
$I->dontSee('Body cannot be blank', '.help-inline');
$I->dontSee('The verification code is incorrect', '.help-inline');
}
public function submitFormSuccessfully(\FunctionalTester $I)
{
$I->submitForm('#contact-form', [
'ContactForm[name]' => 'tester',
'ContactForm[email]' => 'tester@example.com',
'ContactForm[subject]' => 'test subject',
'ContactForm[body]' => 'test content',
'ContactForm[verifyCode]' => 'testme',
]);
$I->seeEmailIsSent();
$I->dontSeeElement('#contact-form');
$I->see('Thank you for contacting us. We will respond to you as soon as possible.');
}
}

View File

@ -0,0 +1,59 @@
<?php
class LoginFormCest
{
public function _before(\FunctionalTester $I)
{
$I->amOnRoute('site/login');
}
public function openLoginPage(\FunctionalTester $I)
{
$I->see('Login', 'h1');
}
// demonstrates `amLoggedInAs` method
public function internalLoginById(\FunctionalTester $I)
{
$I->amLoggedInAs(100);
$I->amOnPage('/');
$I->see('Logout (admin)');
}
// demonstrates `amLoggedInAs` method
public function internalLoginByInstance(\FunctionalTester $I)
{
$I->amLoggedInAs(\app\models\User::findByUsername('admin'));
$I->amOnPage('/');
$I->see('Logout (admin)');
}
public function loginWithEmptyCredentials(\FunctionalTester $I)
{
$I->submitForm('#login-form', []);
$I->expectTo('see validations errors');
$I->see('Username cannot be blank.');
$I->see('Password cannot be blank.');
}
public function loginWithWrongCredentials(\FunctionalTester $I)
{
$I->submitForm('#login-form', [
'LoginForm[username]' => 'admin',
'LoginForm[password]' => 'wrong',
]);
$I->expectTo('see validations errors');
$I->see('Incorrect username or password.');
}
public function loginSuccessfully(\FunctionalTester $I)
{
$I->submitForm('#login-form', [
'LoginForm[username]' => 'admin',
'LoginForm[password]' => 'admin',
]);
$I->see('Logout (admin)');
$I->dontSeeElement('form#login-form');
}
}

View File

@ -0,0 +1 @@
<?php

11
tests/unit.suite.yml Normal file
View File

@ -0,0 +1,11 @@
# Codeception Test Suite Configuration
# suite for unit (internal) tests.
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
actor: UnitTester
modules:
enabled:
- Asserts
- Yii2:
part: [orm, email, fixtures]

View File

@ -0,0 +1,3 @@
<?php
// add unit testing specific bootstrap code here

View File

@ -0,0 +1,41 @@
<?php
namespace tests\unit\models;
use app\models\ContactForm;
use yii\mail\MessageInterface;
class ContactFormTest extends \Codeception\Test\Unit
{
/**
* @var \UnitTester
*/
public $tester;
public function testEmailIsSentOnContact()
{
$model = new ContactForm();
$model->attributes = [
'name' => 'Tester',
'email' => 'tester@example.com',
'subject' => 'very important letter subject',
'body' => 'body of current message',
'verifyCode' => 'testme',
];
verify($model->contact('admin@example.com'))->notEmpty();
// using Yii2 module actions to check email was sent
$this->tester->seeEmailIsSent();
/** @var MessageInterface $emailMessage */
$emailMessage = $this->tester->grabLastSentEmail();
verify($emailMessage)->instanceOf('yii\mail\MessageInterface');
verify($emailMessage->getTo())->arrayHasKey('admin@example.com');
verify($emailMessage->getFrom())->arrayHasKey('noreply@example.com');
verify($emailMessage->getReplyTo())->arrayHasKey('tester@example.com');
verify($emailMessage->getSubject())->equals('very important letter subject');
verify($emailMessage->toString())->stringContainsString('body of current message');
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace tests\unit\models;
use app\models\LoginForm;
class LoginFormTest extends \Codeception\Test\Unit
{
private $model;
protected function _after()
{
\Yii::$app->user->logout();
}
public function testLoginNoUser()
{
$this->model = new LoginForm([
'username' => 'not_existing_username',
'password' => 'not_existing_password',
]);
verify($this->model->login())->false();
verify(\Yii::$app->user->isGuest)->true();
}
public function testLoginWrongPassword()
{
$this->model = new LoginForm([
'username' => 'demo',
'password' => 'wrong_password',
]);
verify($this->model->login())->false();
verify(\Yii::$app->user->isGuest)->true();
verify($this->model->errors)->arrayHasKey('password');
}
public function testLoginCorrect()
{
$this->model = new LoginForm([
'username' => 'demo',
'password' => 'demo',
]);
verify($this->model->login())->true();
verify(\Yii::$app->user->isGuest)->false();
verify($this->model->errors)->arrayHasNotKey('password');
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace tests\unit\models;
use app\models\User;
class UserTest extends \Codeception\Test\Unit
{
public function testFindUserById()
{
verify($user = User::findIdentity(100))->notEmpty();
verify($user->username)->equals('admin');
verify(User::findIdentity(999))->empty();
}
public function testFindUserByAccessToken()
{
verify($user = User::findIdentityByAccessToken('100-token'))->notEmpty();
verify($user->username)->equals('admin');
verify(User::findIdentityByAccessToken('non-existing'))->empty();
}
public function testFindUserByUsername()
{
verify($user = User::findByUsername('admin'))->notEmpty();
verify(User::findByUsername('not-admin'))->empty();
}
/**
* @depends testFindUserByUsername
*/
public function testValidateUser()
{
$user = User::findByUsername('admin');
verify($user->validateAuthKey('test100key'))->notEmpty();
verify($user->validateAuthKey('test102key'))->empty();
verify($user->validatePassword('admin'))->notEmpty();
verify($user->validatePassword('123456'))->empty();
}
}

View File

@ -0,0 +1,261 @@
<?php
namespace tests\unit\widgets;
use app\widgets\Alert;
use Yii;
class AlertTest extends \Codeception\Test\Unit
{
public function testSingleErrorMessage()
{
$message = 'This is an error message';
Yii::$app->session->setFlash('error', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testMultipleErrorMessages()
{
$firstMessage = 'This is the first error message';
$secondMessage = 'This is the second error message';
Yii::$app->session->setFlash('error', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testSingleDangerMessage()
{
$message = 'This is a danger message';
Yii::$app->session->setFlash('danger', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testMultipleDangerMessages()
{
$firstMessage = 'This is the first danger message';
$secondMessage = 'This is the second danger message';
Yii::$app->session->setFlash('danger', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testSingleSuccessMessage()
{
$message = 'This is a success message';
Yii::$app->session->setFlash('success', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testMultipleSuccessMessages()
{
$firstMessage = 'This is the first danger message';
$secondMessage = 'This is the second danger message';
Yii::$app->session->setFlash('success', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testSingleInfoMessage()
{
$message = 'This is an info message';
Yii::$app->session->setFlash('info', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testMultipleInfoMessages()
{
$firstMessage = 'This is the first info message';
$secondMessage = 'This is the second info message';
Yii::$app->session->setFlash('info', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testSingleWarningMessage()
{
$message = 'This is a warning message';
Yii::$app->session->setFlash('warning', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-warning');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
}
public function testMultipleWarningMessages()
{
$firstMessage = 'This is the first warning message';
$secondMessage = 'This is the second warning message';
Yii::$app->session->setFlash('warning', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-warning');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
}
public function testSingleMixedMessages() {
$errorMessage = 'This is an error message';
$dangerMessage = 'This is a danger message';
$successMessage = 'This is a success message';
$infoMessage = 'This is a info message';
$warningMessage = 'This is a warning message';
Yii::$app->session->setFlash('error', $errorMessage);
Yii::$app->session->setFlash('danger', $dangerMessage);
Yii::$app->session->setFlash('success', $successMessage);
Yii::$app->session->setFlash('info', $infoMessage);
Yii::$app->session->setFlash('warning', $warningMessage);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($errorMessage);
verify($renderingResult)->stringContainsString($dangerMessage);
verify($renderingResult)->stringContainsString($successMessage);
verify($renderingResult)->stringContainsString($infoMessage);
verify($renderingResult)->stringContainsString($warningMessage);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringContainsString('alert-success');
verify($renderingResult)->stringContainsString('alert-info');
verify($renderingResult)->stringContainsString('alert-warning');
}
public function testMultipleMixedMessages() {
$firstErrorMessage = 'This is the first error message';
$secondErrorMessage = 'This is the second error message';
$firstDangerMessage = 'This is the first danger message';
$secondDangerMessage = 'This is the second';
$firstSuccessMessage = 'This is the first success message';
$secondSuccessMessage = 'This is the second success message';
$firstInfoMessage = 'This is the first info message';
$secondInfoMessage = 'This is the second info message';
$firstWarningMessage = 'This is the first warning message';
$secondWarningMessage = 'This is the second warning message';
Yii::$app->session->setFlash('error', [$firstErrorMessage, $secondErrorMessage]);
Yii::$app->session->setFlash('danger', [$firstDangerMessage, $secondDangerMessage]);
Yii::$app->session->setFlash('success', [$firstSuccessMessage, $secondSuccessMessage]);
Yii::$app->session->setFlash('info', [$firstInfoMessage, $secondInfoMessage]);
Yii::$app->session->setFlash('warning', [$firstWarningMessage, $secondWarningMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstErrorMessage);
verify($renderingResult)->stringContainsString($secondErrorMessage);
verify($renderingResult)->stringContainsString($firstDangerMessage);
verify($renderingResult)->stringContainsString($secondDangerMessage);
verify($renderingResult)->stringContainsString($firstSuccessMessage);
verify($renderingResult)->stringContainsString($secondSuccessMessage);
verify($renderingResult)->stringContainsString($firstInfoMessage);
verify($renderingResult)->stringContainsString($secondInfoMessage);
verify($renderingResult)->stringContainsString($firstWarningMessage);
verify($renderingResult)->stringContainsString($secondWarningMessage);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringContainsString('alert-success');
verify($renderingResult)->stringContainsString('alert-info');
verify($renderingResult)->stringContainsString('alert-warning');
}
public function testFlashIntegrity()
{
$errorMessage = 'This is an error message';
$unrelatedMessage = 'This is a message that is not related to the alert widget';
Yii::$app->session->setFlash('error', $errorMessage);
Yii::$app->session->setFlash('unrelated', $unrelatedMessage);
Alert::widget();
// Simulate redirect
Yii::$app->session->close();
Yii::$app->session->open();
verify(Yii::$app->session->getFlash('error'))->empty();
verify(Yii::$app->session->getFlash('unrelated'))->equals($unrelatedMessage);
}
}

BIN
vagrant/.DS_Store vendored Normal file

Binary file not shown.

2
vagrant/config/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# local configuration
vagrant-local.yml

View File

@ -0,0 +1,22 @@
# Your personal GitHub token
github_token: <your-personal-github-token>
# Read more: https://github.com/blog/1509-personal-api-tokens
# You can generate it here: https://github.com/settings/tokens
# Guest OS timezone
timezone: Europe/London
# Are we need check box updates for every 'vagrant up'?
box_check_update: false
# Virtual machine name
machine_name: yii2basic
# Virtual machine IP
ip: 192.168.83.137
# Virtual machine CPU cores number
cpus: 1
# Virtual machine RAM
memory: 1024

38
vagrant/nginx/app.conf Normal file
View File

@ -0,0 +1,38 @@
server {
charset utf-8;
client_max_body_size 128M;
sendfile off;
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## listen for ipv6
server_name yii2basic.test;
root /app/web/;
index index.php;
access_log /app/vagrant/nginx/log/yii2basic.access.log;
error_log /app/vagrant/nginx/log/yii2basic.error.log;
location / {
# Redirect everything that isn't a real file to index.php
try_files $uri $uri/ /index.php$is_args$args;
}
# uncomment to avoid processing of calls to non-existing static files by Yii
#location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
# try_files $uri =404;
#}
#error_page 404 /404.html;
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
#fastcgi_pass 127.0.0.1:9000;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
try_files $uri =404;
}
location ~ /\.(ht|svn|git) {
deny all;
}
}

3
vagrant/nginx/log/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
#nginx logs
yii2basic.access.log
yii2basic.error.log

View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
#== Bash helpers ==
function info {
echo " "
echo "--> $1"
echo " "
}
#== Provision script ==
info "Provision-script user: `whoami`"
info "Restart web-stack"
service php7.2-fpm restart
service nginx restart
service mysql restart

View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
#== Import script args ==
timezone=$(echo "$1")
readonly IP=$2
#== Bash helpers ==
function info {
echo " "
echo "--> $1"
echo " "
}
#== Provision script ==
info "Provision-script user: `whoami`"
export DEBIAN_FRONTEND=noninteractive
info "Configure timezone"
timedatectl set-timezone ${timezone} --no-ask-password
info "Add the VM IP to the list of allowed IPs"
awk -v ip=$IP -f /app/vagrant/provision/provision.awk /app/config/web.php
info "Prepare root password for MySQL"
debconf-set-selections <<< 'mariadb-server mysql-server/root_password password'
debconf-set-selections <<< 'mariadb-server mysql-server/root_password_again password'
echo "Done!"
info "Update OS software"
apt-get update
apt-get upgrade -y
info "Install additional software"
apt-get install -y php7.2-curl php7.2-cli php7.2-intl php7.2-mysqlnd php7.2-gd php7.2-fpm php7.2-mbstring php7.2-xml unzip nginx mariadb-server-10.1 php.xdebug
info "Configure MySQL"
sed -i 's/.*bind-address.*/bind-address = 0.0.0.0/' /etc/mysql/mariadb.conf.d/50-server.cnf
mysql <<< "CREATE USER 'root'@'%' IDENTIFIED BY ''"
mysql <<< "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'"
mysql <<< "DROP USER 'root'@'localhost'"
mysql <<< 'FLUSH PRIVILEGES'
echo "Done!"
info "Configure PHP-FPM"
sed -i 's/user = www-data/user = vagrant/g' /etc/php/7.2/fpm/pool.d/www.conf
sed -i 's/group = www-data/group = vagrant/g' /etc/php/7.2/fpm/pool.d/www.conf
sed -i 's/owner = www-data/owner = vagrant/g' /etc/php/7.2/fpm/pool.d/www.conf
cat << EOF > /etc/php/7.2/mods-available/xdebug.ini
zend_extension=xdebug.so
xdebug.remote_enable=1
xdebug.remote_connect_back=1
xdebug.remote_port=9000
xdebug.remote_autostart=1
EOF
echo "Done!"
info "Configure NGINX"
sed -i 's/user www-data/user vagrant/g' /etc/nginx/nginx.conf
echo "Done!"
info "Enabling site configuration"
ln -s /app/vagrant/nginx/app.conf /etc/nginx/sites-enabled/app.conf
echo "Done!"
info "Removing default site configuration"
rm /etc/nginx/sites-enabled/default
echo "Done!"
info "Initialize databases for MySQL"
mysql <<< 'CREATE DATABASE yii2basic'
mysql <<< 'CREATE DATABASE yii2basic_test'
echo "Done!"
info "Install composer"
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

View File

@ -0,0 +1,31 @@
#!/usr/bin/env bash
#== Import script args ==
github_token=$(echo "$1")
#== Bash helpers ==
function info {
echo " "
echo "--> $1"
echo " "
}
#== Provision script ==
info "Provision-script user: `whoami`"
info "Configure composer"
composer config --global github-oauth.github.com ${github_token}
echo "Done!"
info "Install project dependencies"
cd /app
composer --no-progress --prefer-dist install
info "Create bash-alias 'app' for vagrant user"
echo 'alias app="cd /app"' | tee /home/vagrant/.bash_aliases
info "Enabling colorized prompt for guest console"
sed -i "s/#force_color_prompt=yes/force_color_prompt=yes/" /home/vagrant/.bashrc

View File

@ -0,0 +1,50 @@
###
# Modifying Yii2's files for Vagrant VM
#
# @author HA3IK <golubha3ik@gmail.com>
# @version 1.0.0
BEGIN {
print "AWK BEGINs its work:"
IGNORECASE = 1
# Correct IP - wildcard last octet
match(ip, /(([0-9]+\.)+)/, arr)
ip = arr[1] "*"
}
# BODY
{
# Check if it's the same file
if (FILENAME != isFile["same"]){
msg = "- Work with: " FILENAME
# Close a previous file
close(isFile["same"])
# Delete previous data
delete isFile
# Save current file
isFile["same"] = FILENAME
# Define array index for the file
switch (FILENAME){
case /config\/web\.php$/:
isFile["IsConfWeb"] = 1
msg = msg " - add allowed IP: " ip
break
}
# Print the concatenated message for the file
print msg
}
# IF config/web.php
if (isFile["IsConfWeb"]){
# IF line has "allowedIPs" and doesn't has our IP
if (match($0, "allowedIPs") && !match($0, ip)){
match($0, /([^\]]+)(.+)/, arr)
$0 = sprintf("%s, '%s'%s", arr[1], ip, arr[2])
}
# Rewrite the file
print $0 > FILENAME
}
}
END {
print "AWK ENDs its work."
}

22
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit5de2040ea43272234872b658d6c40998::getLoader();

605
vendor/behat/gherkin/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,605 @@
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
This project follows the [Behat release and version support policies]
(https://docs.behat.org/en/latest/releases.html).
# [4.16.1] - 2025-12-08
### Fixed
* Reinstate support for tag filter expressions without a leading `@` (e.g. `wip&&~slow` instead of `@wip&&~@slow`).
This syntax was never officially supported, but previously worked and was broken by 4.16.0. We have temporarily
fixed this, but it is deprecated and will be removed in the next major version.
# [4.16.0] - 2025-12-05
### Changed
* Further improvements to parser parity when the experimental `gherkin-32` compatibility mode is enabled:
- Parse descriptions (instead of multiline titles) for all describable nodes by @acoulton in [#361](https://github.com/Behat/Gherkin/pull/361)
- Unescape escaped delimiters within doc strings by @stof in [#393](https://github.com/Behat/Gherkin/pull/393)
- Retain the `@` prefix when parsing tags by @acoulton in [#400](https://github.com/Behat/Gherkin/pull/400)
- Trim unicode padding from table cells by @stof in [#405](https://github.com/Behat/Gherkin/pull/405)
### Fixed
* Fix the implementation of the default dialect for the keywords provider by @stof in [#404](https://github.com/Behat/Gherkin/pull/404)
### Internal
* Add `Stringable` to classes implementing __toString() by @acoulton in [#402](https://github.com/Behat/Gherkin/pull/402)
* Fix cucumber variant assertions to include inherited properties by @acoulton in [#394](https://github.com/Behat/Gherkin/pull/394)
* Update cucumber/gherkin parity tests to 37.0.0 by @behat-gherkin-updater[bot] in [#397](https://github.com/Behat/Gherkin/pull/397) and [#398](https://github.com/Behat/Gherkin/pull/398)
* update_cucumber script should not fail on manually created releases by @acoulton in [#396](https://github.com/Behat/Gherkin/pull/396)
* Add funding links and information by @acoulton in [#401](https://github.com/Behat/Gherkin/pull/401)
# [4.15.0] - 2025-11-05
### Changed
* Added a new ParserInterface and deprecated extending the core Lexer, Parser and Node classes by @acoulton in [#354](https://github.com/Behat/Gherkin/pull/354)
* Deprecate the CucumberNDJsonAstLoader (which was only intended for internal use by our tests) by @stof in [#356](https://github.com/Behat/Gherkin/pull/356)
* By default, the parser ignores invalid language tags (e.g. `#language:no-such`) and falls back to the default language
(e.g. `en`). Previously, the resultant `FeatureNode::getLanguage()` would return the original invalid value from the
feature file - it will now return the language that was actually used for parsing. By @stof in [#350](https://github.com/Behat/Gherkin/pull/350)
### Added
* Introduce a DialectProviderInterface matching the modern cucumber API. This will replace the existing Keywords API in
a future major release. By @stof in [#350](https://github.com/Behat/Gherkin/pull/350)
* Introduce configurable `GherkinCompatibilityMode` to control how gherkin files are parsed. In the default `legacy` mode,
there is no change to parsing. In the new **experimental** `gherkin-32` mode, files will in future be parsed
consistently with the official cucumber/gherkin parsers. This mode is not yet complete - in this first release:
- Whitespace within description nodes will not be trimmed by @acoulton in [#349](https://github.com/Behat/Gherkin/pull/349)
- Invalid language tags will cause an exception by @stof in [#357](https://github.com/Behat/Gherkin/pull/357)
- Step keywords will not be trimmed by @stof in [#360](https://github.com/Behat/Gherkin/pull/360)
- Language tags can include whitespace by @acoulton in [#358](https://github.com/Behat/Gherkin/pull/358)
- `\n` literals in table cells will be parsed as newlines by @stof in [#359](https://github.com/Behat/Gherkin/pull/359)
and [#391](https://github.com/Behat/Gherkin/pull/391)
* Improved translations for `ru` (Russian) and `af` (Afrikaans) from cucumber/gherkin in [#381](https://github.com/Behat/Gherkin/pull/381)
and [#386](https://github.com/Behat/Gherkin/pull/386)
* Support PHP 8.5 by @acoulton in [#388](https://github.com/Behat/Gherkin/pull/388)
### Fixed
* Improve phpdoc / phpstan type-hinting of the lexer and parser by @uuf6429 in [#344](https://github.com/Behat/Gherkin/pull/344)
and @stof in [#363](https://github.com/Behat/Gherkin/pull/363)
* Handle race conditions when creating cache directory by @uuf6429 in [#373](https://github.com/Behat/Gherkin/pull/373)
* Throw if Loader->load() called with unsupported resource by @uuf6429 in [#372](https://github.com/Behat/Gherkin/pull/372)
* Use default file cache key if `behat/gherkin` version is unknown by @uuf6429 in [#370](https://github.com/Behat/Gherkin/pull/370)
### Internal
* Enable PHPStan level 10 and resolve remaining warnings by @uuf6429 in [#368](https://github.com/Behat/Gherkin/pull/368)
* Remove duplication and improve robustness in filesystem operations by @uuf6429 in [#365](https://github.com/Behat/Gherkin/pull/365)
and [#367](https://github.com/Behat/Gherkin/pull/367)
* Explicitly cover expected departures from cucumber gherkin parsing with tests by @acoulton in [#392](https://github.com/Behat/Gherkin/pull/392)
* Update cucumber/gherkin parity tests to v36.0.0 in [#355](https://github.com/Behat/Gherkin/pull/355), [#376](https://github.com/Behat/Gherkin/pull/376)
[#378](https://github.com/Behat/Gherkin/pull/378), [#381](https://github.com/Behat/Gherkin/pull/381), [#385](https://github.com/Behat/Gherkin/pull/385)
[#386](https://github.com/Behat/Gherkin/pull/386) and [#387](https://github.com/Behat/Gherkin/pull/387)
* Fixes and improvements to the cucumber update CI job by @acoulton in [#374](https://github.com/Behat/Gherkin/pull/374),
[#375](https://github.com/Behat/Gherkin/pull/375), [#379](https://github.com/Behat/Gherkin/pull/379)
and [#380](https://github.com/Behat/Gherkin/pull/380)
* Minor coding style fixes by @acoulton in [#377](https://github.com/Behat/Gherkin/pull/377) and [#383](https://github.com/Behat/Gherkin/pull/383)
* Minor code improvements to Lexer/Parser implementation by @uuf6429 in [#352](https://github.com/Behat/Gherkin/pull/352)
* Minor code improvements to TableNode by @uuf6429 in [#366](https://github.com/Behat/Gherkin/pull/366)
* Add native typehints where this does not break BC by @stof in [#353](https://github.com/Behat/Gherkin/pull/353)
* Fix typo of a PHPStan alias type by @uuf6429 in [#371](https://github.com/Behat/Gherkin/pull/371)
* Fix github actions workflow job name by @uuf6429 in [#369](https://github.com/Behat/Gherkin/pull/369)
# [4.14.0] - 2025-05-23
### Changed
* Throw ParserException if file ends with tags by @acoulton in [#313](https://github.com/Behat/Gherkin/pull/313)
* Throw ParserException if Background comes after first Scenario by @acoulton in [#343](https://github.com/Behat/Gherkin/pull/343)
* For compatibility with the official cucumber/gherkin parsers, we now accept some gherkin syntax that would previously
have triggered a ParserException. Users may wish to consider running a tool like gherkin-lint in CI to detect
incomplete feature files or valid-but-unusual gherkin syntax. The specific changes are:
- Parse `Scenario` and `Scenario Outline` as synonyms depending on the presence (or not) of an `Examples:` keyword.
by @acoulton in [#316](https://github.com/Behat/Gherkin/pull/316) and [#324](https://github.com/Behat/Gherkin/pull/324)
- Do not throw on some unexpected Feature / Language tags by @acoulton in [#323](https://github.com/Behat/Gherkin/pull/323)
- Do not throw on `.feature` file that does not contain a Feature by @acoulton in [#340](https://github.com/Behat/Gherkin/pull/340)
- Ignore content after table right-hand `|` (instead of throwing) by @acoulton in [#341](https://github.com/Behat/Gherkin/pull/341)
* Remove the line length from the NewLine token value by @stof in [#338](https://github.com/Behat/Gherkin/pull/338)
* Added precise PHPStan type information by @stof in [#332](https://github.com/Behat/Gherkin/pull/332),
[#333](https://github.com/Behat/Gherkin/pull/333), [#339](https://github.com/Behat/Gherkin/pull/339)
and [#334](https://github.com/Behat/Gherkin/pull/334)
### Internal
* Make private props readonly; fix tests by @uuf6429 in [#319](https://github.com/Behat/Gherkin/pull/319)
* Use the `Yaml::parseFile` API to handle Yaml files by @stof in [#335](https://github.com/Behat/Gherkin/pull/335)
* test: Make CucumberND name reading consistent by @uuf6429 in [#309](https://github.com/Behat/Gherkin/pull/309)
* test: Use vfsStream to simplify / improve filesystem-related tests by @uuf6429 in [#298](https://github.com/Behat/Gherkin/pull/298)
* test: Handle optional tableHeader when loading NDJson examples by @uuf6429 in [#294](https://github.com/Behat/Gherkin/pull/294)
* test: Refactor valid ParserExceptionsTest examples into cucumber/gherkin testdata by @acoulton in [#322](https://github.com/Behat/Gherkin/pull/322)
* test: Compare step arguments when checking gherkin parity by @acoulton in [#325](https://github.com/Behat/Gherkin/pull/325)
* test: Use a custom object comparator to ignore the keywordType of StepNode by @stof in [#331](https://github.com/Behat/Gherkin/pull/331)
* ci: Add conventional title to gherkin update, error on missing asserts by @acoulton in [#314](https://github.com/Behat/Gherkin/pull/314)
* Assert that preg_split does not fail when splitting a table row by @stof in [#337](https://github.com/Behat/Gherkin/pull/337)
* Add assertions in the parser to reflect the structure of tokens by @stof in [#342](https://github.com/Behat/Gherkin/pull/342)
* style: Define and change phpdoc order coding style by @uuf6429 in [#345](https://github.com/Behat/Gherkin/pull/345)
# [4.13.0] - 2025-05-06
### Changed
* Files have been moved to flatten paths into a PSR-4 structure (instead of the previous PSR-0). This may affect users
who are requiring files directly rather than using the composer autoloader as expected.
See the 4.12.0 release for the new `CachedArrayKeywords::withDefaultKeywords()` to use the `i18n.php` file without
depending on paths to other files in this repo. By @uuf6429 in [#288](https://github.com/Behat/Gherkin/pull/288)
### Added
* ExampleTableNode now implements TaggedNodeInterface. Also refactored node tag handling methods. By @uuf6429 in
[#289](https://github.com/Behat/Gherkin/pull/289)
* Improve some exceptions thrown when parsing invalid feature files. Also increased test coverage. By @uuf6429 in
[#295](https://github.com/Behat/Gherkin/pull/295)
* New translations for `amh` (Amharic), `be` (Belarusian) and `ml` (Malayalam) from cucumber/gherkin in [#306](https://github.com/Behat/Gherkin/pull/306)
* Improved translations / whitespace for `ga` (Irish), `it` (Italian), `ja` (Japanese), `ka` (Georgian) and `ko` (Korean)
from cucumber/gherkin in [#306](https://github.com/Behat/Gherkin/pull/306)
### Internal
* Fix & improve automatic CI updates to newer cucumber/gherkin test data and translations. By @acoulton in
[#300](https://github.com/Behat/Gherkin/pull/300), [#302](https://github.com/Behat/Gherkin/pull/302),
[#304](https://github.com/Behat/Gherkin/pull/304), [#305](https://github.com/Behat/Gherkin/pull/305)
* Update code style and resolve PHPStan warnings (up to level 9) in tests and CI scripts. By @uuf6429 in
[#296](https://github.com/Behat/Gherkin/pull/296), [#297](https://github.com/Behat/Gherkin/pull/297)
and [#307](https://github.com/Behat/Gherkin/pull/307)
* Make tests that expect exceptions more explicit by @uuf6429 in [#310](https://github.com/Behat/Gherkin/pull/310)
* Improve CI workflows and integrate Codecov reporting by @uuf6429 in [#299](https://github.com/Behat/Gherkin/pull/299)
and [#301](https://github.com/Behat/Gherkin/pull/301)
* Refactor tag filtering implementation by @uuf6429 in [#308](https://github.com/Behat/Gherkin/pull/308)
* Update cucumber/gherkin parity tests to v32.1.1 in [#306](https://github.com/Behat/Gherkin/pull/306)
# [4.12.0] - 2025-02-26
### Changed
* Gherkin::VERSION is deprecated and will not be updated, use the composer runtime API if you need to identify the
running version. This also changes the value used to namespace cached feature files.
by @acoulton in [#279](https://github.com/Behat/Gherkin/pull/279)
### Added
* Provide `CachedArrayKeywords::withDefaultKeywords()` to create an instance without an external dependency on the path
to the `i18n.php` file in this repo. **NOTE** that paths to source files will change in the next Gherkin release -
use the new constructor to avoid any impact.
by @carlos-granados in [#290](https://github.com/Behat/Gherkin/pull/290)
### Internal
* Upgrade to phpunit 10 by @uuf6429 in [#275](https://github.com/Behat/Gherkin/pull/275)
* Remove redundant files by @uuf6429 in [#278](https://github.com/Behat/Gherkin/pull/278)
* Update documentation by @uuf6429 in [#274](https://github.com/Behat/Gherkin/pull/274)
* Adopt PHP CS Fixer and apply code styles by @uuf6429 in [#277](https://github.com/Behat/Gherkin/pull/277)
* Add PHPStan and improve / fix docblock annotations and type-safety within methods to achieve level 5 by
@uuf6429 in [#276](https://github.com/Behat/Gherkin/pull/276), [#281](https://github.com/Behat/Gherkin/pull/281),
[#282](https://github.com/Behat/Gherkin/pull/282), and [#287](https://github.com/Behat/Gherkin/pull/287)
# [4.11.0] - 2024-12-06
### Changed
* Drop support for PHP < 8.1, Symfony < 5.4 and Symfony 6.0 - 6.3. In future we will drop support for PHP and symfony
versions as they reach EOL. by @acoulton in [#272](https://github.com/Behat/Gherkin/pull/272)
* Deprecated `ExampleNode::getTitle()` and `ScenarioNode::getTitle()` in favour of new methods with clearer meaning.
by @uuf6429 in [#271](https://github.com/Behat/Gherkin/pull/271)
### Added
* Added `(ExampleNode|ScenarioNode)::getName()` to access human-readable names for examples and scenarios,
and `ExampleNode::getExampleText()` for the string content of the example table row.
by @uuf6429 in [#271](https://github.com/Behat/Gherkin/pull/271)
### Internal
* Enable dependabot for github actions workflows by @jrfnl in [#261](https://github.com/Behat/Gherkin/pull/261)
# 4.10.0 / 2024-10-19
### Changed
- **⚠ Backslashes in feature files must now be escaped**\
Gherkin syntax treats `\` as an escape character, which must be escaped (`\\`) to use it as a
literal value. Historically, this was not being parsed correctly. This release fixes that bug,
but means that if your scenarios currently use unescaped `\` you will need to replace each one
with `\\` to achieve the same parsed result.
By @everzet in 5a0836d.
### Added
- Symfony 6 and 7 thanks to @tacman in #257
- PHP 8.4 support thanks to @heiglandreas in #258 and @jrfnl in #262
### Fixed
- Fix exception when filter string is empty thanks to @magikid in #251
### Internal
- Sync teststuite with Cucumber 24.1.0
- Fix PHPUnit 10 deprecation messages
- A lot of great CI work by @heiglandreas and @jrfnl
# 4.9.0 / 2021-10-12
- Simplify the boolean condition for the tag matching by @stof in https://github.com/Behat/Gherkin/pull/219
- Remove symfony phpunit bridge by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/220
- Ignore the bin folder in archives by @stof in https://github.com/Behat/Gherkin/pull/226
- Cast table node exceptions into ParserExceptions when throwing by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/216
- Cucumber changelog in PRs and using correct hash by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/225
- Support alternative docstrings format (```) by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/214
- Fix DocBlocks (Boolean -> bool) by @simonhammes in https://github.com/Behat/Gherkin/pull/237
- Tag parsing by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/215
- Remove test - cucumber added an example with Rule which is not supported by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/239
- Add PHP 8.1 support by @javer in https://github.com/Behat/Gherkin/pull/242
- Fix main branch alias version by @mvorisek in https://github.com/Behat/Gherkin/pull/244
# 4.8.0 / 2021-02-04
- Drop support for PHP before version 7.2
# 4.7.3 / 2021-02-04
- Refactored comments parsing to avoid Maximum function nesting level errors
# 4.7.2 / 2021-02-03
- Issue where Scenario Outline title was not populated into Examples
- Updated translations from cucumber 16.0.0
# 4.7.1 / 2021-01-26
- Issue parsing comments before scenarios when following an Examples table
# 4.7.0 / 2021-01-24
- Provides better messages for TableNode construct errors
- Now allows single character steps
- Supports multiple Example Tables with tags
# 4.6.2 / 2020-03-17
- Fixed issues due to incorrect cache key
# 4.6.1 / 2020-02-27
- Fix AZ translations
- Correctly filter features, now that the base path is correctly set
# 4.6.0 / 2019-01-16
- Updated translations (including 'Example' as synonym for 'Scenario' in `en`)
# 4.5.1 / 2017-08-30
- Fix regression in `PathsFilter`
# 4.5.0 / 2017-08-30
- Sync i18n with Cucumber Gherkin
- Drop support for HHVM tests on Travis
- Add `TableNode::fromList()` method (thanks @TravisCarden)
- Add `ExampleNode::getOutlineTitle()` method (thanks @duxet)
- Use realpath, so the feature receives the cwd prefixed (thanks @glennunipro)
- Explicitly handle non-two-dimensional arrays in TableNode (thanks @TravisCarden)
- Fix to line/linefilter scenario runs which take relative paths to files (thanks @generalconsensus)
# 4.4.5 / 2016-10-30
- Fix partial paths matching in `PathsFilter`
# 4.4.4 / 2016-09-18
- Provide clearer exception for non-writeable cache directories
# 4.4.3 / 2016-09-18
- Ensure we reset tags between features
# 4.4.2 / 2016-09-03
- Sync 18n with gherkin 3
# 4.4.1 / 2015-12-30
- Ensure keywords are trimmed when syncing translations
- Sync 18n with cucumber
# 4.4.0 / 2015-09-19
- Added validation enforcing that all rows of a `TableNode` have the same number of columns
- Added `TableNode::getColumn` to get a column from the table
- Sync 18n with cucumber
# 4.3.0 / 2014-06-06
- Added `setFilters(array)` method to `Gherkin` class
- Added `NarrativeFilter` for non-english `RoleFilter` lovers
# 4.2.1 / 2014-06-06
- Fix parsing of features without line feed at the end
# 4.2.0 / 2014-05-27
- Added `getKeyword()` and `getKeywordType()` methods to `StepNode`, deprecated `getType()`.
Thanks to @kibao
# 4.1.3 / 2014-05-25
- Properly handle tables with rows terminating in whitespace
# 4.1.2 / 2014-05-14
- Handle case where Gherkin cache is broken
# 4.1.1 / 2014-05-05
- Fixed the compatibility with PHP 5.6-beta by avoiding to use the broken PHP array function
- The YamlFileLoader no longer extend from ArrayLoader but from AbstractFileLoader
# 4.1.0 / 2014-04-20
- Fixed scenario tag filtering
- Do not allow multiple multiline step arguments
- Sync 18n with cucumber
# 4.0.0 / 2014-01-05
- Changed the behavior when no loader can be found for the resource. Instead of throwing an exception, the
Gherkin class now returns an empty array.
# 3.1.3 / 2014-01-04
- Dropped the dependency on the Symfony Finder by using SPL iterators directly
- Added testing on HHVM on Travis. HHVM is officially supported (previous release was actually already compatible)
# 3.1.2 / 2014-01-01
- All paths passed to PathsFilter are converted using realpath
# 3.1.1 / 2013-12-31
- Add `ComplexFilterInterace` that has complex behavior for scenarios and requires to pass
feature too
- `TagFilter` is an instance of a `ComplexFilterInterace` now
# 3.1.0 / 2013-12-31
- Example node is a scenario
- Nodes do not have uprefs (memory usage fix)
- Scenario filters do not depend on feature nodes
# 3.0.5 / 2014-01-01
- All paths passed to PathsFilter are converted using realpath
# 3.0.4 / 2013-12-31
- TableNode is now traversable using foreach
- All possibly thrown exceptions implement Gherkin\Exception interface
- Sync i18n with cucumber
# 3.0.3 / 2013-09-15
- Extend ExampleNode with additional methods
# 3.0.2 / 2013-09-14
- Extract `KeywordNodeInterface` and `ScenarioLikeInterface`
- Add `getIndex()` methods to scenarios, outlines, steps and examples
- Throw proper exception for fractured node tree
# 3.0.1 / 2013-09-14
- Use versioned subfolder in FileCache
# 3.0.0 / 2013-09-14
- A lot of optimizations in Parser and Lexer
- Node tree is now immutable by nature (no setters)
- Example nodes are now part of the node tree. They are lazily generated by Outline node
- Sync with latest cucumber i18n
# 2.3.4 / 2013-08-11
- Fix leaks in memory cache
# 2.3.3 / 2013-08-11
- Fix encoding bug introduced with previous release
- Sync i18n with cucumber
# 2.3.2 / 2013-08-11
- Explicitly use utf8 encoding
# 2.3.1 / 2013-08-10
- Support `an` prefix with RoleFilter
# 2.3.0 / 2013-08-04
- Add RoleFilter
- Add PathsFilter
- Add MemoryCache
# 2.2.9 / 2013-03-02
- Fix dependency version requirement
# 2.2.8 / 2013-03-02
- Features filtering behavior change. Now emptified (by filtering) features
that do not match filter themselves are removed from resultset.
- Small potential bug fix in TableNode
# 2.2.7 / 2013-01-27
- Fixed bug in i18n syncing script
- Resynced Gherkin i18n
# 2.2.6 / 2013-01-26
- Support long row hashes in tables ([see](https://github.com/Behat/Gherkin/issues/40))
- Synced Gherkin i18n
# 2.2.5 / 2012-09-26
- Fixed issue with loading empty features
- Synced Gherkin i18n
# 2.2.4 / 2012-08-03
- Fixed exception message for "no loader found"
# 2.2.3 / 2012-08-03
- Fixed minor loader bug with empty base path
- Synced Gherkin i18n
# 2.2.2 / 2012-07-01
- Added ability to filter outline scenarios by line and range filters
- Synced Gherkin i18n
- Refactored table parser to read row line numbers too
# 2.2.1 / 2012-05-04
- Fixed StepNode `getLanguage()` and `getFile()`
# 2.2.0 / 2012-05-03
- Features freeze after parsing
- Implemented GherkinDumper (@Halleck45)
- Synced i18n with Cucumber
- Updated inline documentation
# 2.1.1 / 2012-03-09
- Fixed caching bug, where `isFresh()` always returned false
# 2.1.0 / 2012-03-09
- Added parser caching layer
- Added support for table delimiter escaping (use `\|` for that)
- Added LineRangeFilter (thanks @headrevision)
- Synced i18n dictionary with cucumber/gherkin
# 2.0.2 / 2012-02-04
- Synced i18n dictionary with cucumber/gherkin
# 2.0.1 / 2012-01-26
- Fixed issue about parsing features without indentation
# 2.0.0 / 2012-01-19
- Background titles support
- Correct parsing of titles/descriptions (hirarchy lexing)
- Migration to the cucumber/gherkin i18n dictionary
- Speed optimizations
- Refactored KeywordsDumper
- New loaders
- Bugfixes
# 1.1.4 / 2012-01-08
- Read feature description even if it looks like a step
# 1.1.3 / 2011-12-14
- Removed file loading routines from Parser (fixes `is_file()` issue on some systems - thanks
@flodocteurklein)
# 1.1.2 / 2011-12-01
- Updated spanish trasnaltion (@anbotero)
- Integration with Composer and Travis CI
# 1.1.1 / 2011-07-29
- Updated pt language step types (@danielcsgomes)
- Updated vendors
# 1.1.0 / 2011-07-16
- Return all tags, including inherited in `Scenario::getTags()`
- New `Feature::getOwnTags()` and `Scenario::getOwnTags()` method added,
which returns only own tags
# 1.0.8 / 2011-06-29
- Fixed comments parsing.
You cant have comments at the end of a line # like this
# But you can still have comments at the beginning of a line
# 1.0.7 / 2011-06-28
- Added `getRaw()` method to PyStringNode
- Updated vendors
# 1.0.6 / 2011-06-17
- Updated vendors
# 1.0.5 / 2011-06-10
- Fixed bug, introduced with 1.0.4 - hash in PyStrings
# 1.0.4 / 2011-06-10
- Fixed inability to comment pystrings
# 1.0.3 / 2011-04-21
- Fixed introduced with 1.0.2 pystring parsing bug
# 1.0.2 / 2011-04-18
- Fixed bugs in text with comments parsing
# 1.0.1 / 2011-04-01
- Updated vendors
# 1.0.0 / 2011-03-08
- Updated vendors
# 1.0.0RC2 / 2011-02-25
- Windows support
- Missing phpunit config
# 1.0.0RC1 / 2011-02-15
- Huge optimizations to Lexer & Parser
- Additional loaders (Yaml, Array, Directory)
- Filters (Tag, Name, Line)
- Code refactoring
- Nodes optimizations
- Additional tests for exceptions and translations
- Keywords dumper
# 0.2.0 / 2011-01-05
- New Parser & Lexer (based on AST)
- New verbose parsing exception handling
- New translation mechanics
- 47 brand new translations (see i18n)
- Full test suite for everything from AST nodes to translations
[4.16.1]: https://github.com/Behat/Gherkin/compare/v4.16.0...v4.16.1
[4.16.0]: https://github.com/Behat/Gherkin/compare/v4.15.0...v4.16.0
[4.15.0]: https://github.com/Behat/Gherkin/compare/v4.14.0...v4.15.0
[4.14.0]: https://github.com/Behat/Gherkin/compare/v4.13.0...v4.14.0
[4.13.0]: https://github.com/Behat/Gherkin/compare/v4.12.0...v4.13.0
[4.12.0]: https://github.com/Behat/Gherkin/compare/v4.11.0...v4.12.0
[4.11.0]: https://github.com/Behat/Gherkin/compare/v4.10.0...v4.11.0

22
vendor/behat/gherkin/LICENSE vendored Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2011-2013 Konstantin Kudryashov <ever.zet@gmail.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

81
vendor/behat/gherkin/README.md vendored Normal file
View File

@ -0,0 +1,81 @@
# Behat Gherkin Parser
This is the php Gherkin parser for Behat. It comes bundled with more than 40 native languages (see `i18n.php`) support
and clean architecture.
## Useful Links
- [Behat Site](https://behat.org)
- [Note on Patches/Pull Requests](CONTRIBUTING.md)
## Usage Example
```php
<?php
$keywords = new Behat\Gherkin\Keywords\ArrayKeywords(array(
'en' => array(
'feature' => 'Feature',
'background' => 'Background',
'scenario' => 'Scenario',
'scenario_outline' => 'Scenario Outline|Scenario Template',
'examples' => 'Examples|Scenarios',
'given' => 'Given',
'when' => 'When',
'then' => 'Then',
'and' => 'And',
'but' => 'But'
),
'en-pirate' => array(
'feature' => 'Ahoy matey!',
'background' => 'Yo-ho-ho',
'scenario' => 'Heave to',
'scenario_outline' => 'Shiver me timbers',
'examples' => 'Dead men tell no tales',
'given' => 'Gangway!',
'when' => 'Blimey!',
'then' => 'Let go and haul',
'and' => 'Aye',
'but' => 'Avast!'
)
));
$lexer = new Behat\Gherkin\Lexer($keywords);
$parser = new Behat\Gherkin\Parser($lexer);
$feature = $parser->parse(file_get_contents('some.feature'));
```
## Installing Dependencies
```shell
curl https://getcomposer.org/installer | php
php composer.phar update
```
Contributors
------------
- Konstantin Kudryashov [everzet](https://github.com/everzet) [original developer]
- Andrew Coulton [acoulton](https://github.com/acoulton) [current maintainer]
- Carlos Granados [carlos-granados](https://github.com/carlos-granados) [current maintainer]
- Christophe Coevoet [stof](https://github.com/stof) [current maintainer]
- Other [awesome developers](https://github.com/Behat/Gherkin/graphs/contributors)
Support the project
-------------------
Behat is free software, maintained by volunteers as a gift for users. If you'd like to see
the project continue to thrive, and particularly if you use it for work, we'd encourage you
to contribute.
Contributions of time - whether code, documentation, or support reviewing PRs and triaging
issues - are very welcome and valued by the maintainers and the wider Behat community.
But we also believe that [financial sponsorship is an important part of a healthy Open Source
ecosystem](https://opensourcepledge.com/about/). Maintaining a project like Behat requires a
significant commitment from the core team: your support will help us to keep making that time
available over the long term. Even small contributions make a big difference.
You can support [@acoulton](https://github.com/acoulton), [@carlos-granados](https://github.com/carlos-granados) and
[@stof](https://github.com/stof) on GitHub sponsors. If you'd like to discuss supporting us in a different way, please
get in touch!

Some files were not shown because too many files have changed in this diff Show More