Merge branch 'master' into feature/pcntl-req-check

This commit is contained in:
Tobias Munk
2023-09-21 17:04:07 +02:00
committed by GitHub
107 changed files with 4583 additions and 2306 deletions

View File

@ -4,95 +4,104 @@ on: [push, pull_request]
env:
DEFAULT_COMPOSER_FLAGS: "--prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi"
PHPUNIT_EXCLUDE_GROUP: mssql,oci,wincache,xcache,zenddata,cubrid
PHPUNIT_EXCLUDE_GROUP: db,wincache,xcache,zenddata
XDEBUG_MODE: coverage, develop
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
phpunit:
name: PHP ${{ matrix.php }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
services:
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: yiitest
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
postgres:
image: postgres:9.6
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: yiitest
ports:
- 5432:5432
options: --name=postgres --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
php: [5.4, 5.5, 5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1]
include:
- php: 5.4
coverage: none
extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 5.5
coverage: none
extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 5.6
coverage: none
extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 7.0
coverage: none
extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 7.1
coverage: none
extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 7.2
coverage: none
extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 7.3
coverage: none
extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 7.4
coverage: xdebug
extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 8.0
coverage: none
extensions: apcu, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 8.1
coverage: none
extensions: apcu, curl, dom, imagick, intl, mbstring, mcrypt, memcached
os: ubuntu-latest
- php: 8.2
extensions: apcu, curl, dom, imagick, intl, mbstring, mcrypt, memcached
coverage: none
os: ubuntu-latest
steps:
- name: Generate french locale
- name: Generate french locale.
run: sudo locale-gen fr_FR.UTF-8
- name: Checkout
- name: Checkout.
uses: actions/checkout@v3
- name: Install PHP
- name: Install PHP.
uses: shivammathur/setup-php@v2
with:
coverage: ${{ matrix.coverage }}
extensions: ${{ matrix.extensions }}
ini-values: apc.enabled=1,apc.shm_size=32M,apc.enable_cli=1, date.timezone='UTC', session.save_path="${{ runner.temp }}"
php-version: ${{ matrix.php }}
tools: pecl
extensions: apc, curl, dom, imagick, intl, mbstring, mcrypt, memcached, mysql, pdo, pdo_mysql, pdo_pgsql, pdo_sqlite, pgsql, sqlite
ini-values: date.timezone='UTC', session.save_path="${{ runner.temp }}"
- name: Install Memcached
- name: Install Memcached.
uses: niden/actions-memcached@v7
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
- name: Install dependencies.
run: composer update $DEFAULT_COMPOSER_FLAGS
- name: PHP Unit tests
- name: Run tests with PHPUnit.
if: matrix.php < '7.4' || matrix.php >= '8.1'
run: vendor/bin/phpunit --verbose --exclude-group $PHPUNIT_EXCLUDE_GROUP --colors=always
npm:
name: NPM 6 on ubuntu-latest
runs-on: ubuntu-latest
- name: Run tests with PHPUnit.
if: matrix.php == '8.0'
run: vendor/bin/phpunit --verbose --exclude-group $PHPUNIT_EXCLUDE_GROUP --colors=always
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install PHP
uses: shivammathur/setup-php@v2
- name: Run tests with PHPUnit and generate coverage.
if: matrix.php == '7.4'
run: vendor/bin/phpunit --verbose --exclude-group $PHPUNIT_EXCLUDE_GROUP --coverage-clover=coverage.xml --colors=always
- name: Upload coverage to Codecov.
if: matrix.php == '7.4'
uses: codecov/codecov-action@v3
with:
php-version: 7.2
ini-values: session.save_path=${{ runner.temp }}
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer update $DEFAULT_COMPOSER_FLAGS
- name: Install node.js
uses: actions/setup-node@v1
with:
node-version: 6
- name: Tests
run: |
npm install
npm test
# env:
# CI: true
file: ./coverage.xml

View File

@ -4,42 +4,31 @@ on:
name: ci-mssql
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: PHP ${{ matrix.php }}-mssql-${{ matrix.mssql }}
env:
key: cache
EXTENSIONS: pdo, pdo_sqlsrv
XDEBUG_MODE: coverage, develop
runs-on: ubuntu-latest
strategy:
matrix:
include:
- php: 7.0
extensions: pdo, pdo_sqlsrv-5.8.1
mssql: server:2017-latest
- php: 7.1
extensions: pdo, pdo_sqlsrv-5.8.1
mssql: server:2017-latest
- php: 7.2
extensions: pdo, pdo_sqlsrv-5.8.1
mssql: server:2017-latest
- php: 7.3
extensions: pdo, pdo_sqlsrv-5.8.1
mssql: server:2017-latest
- php: 7.4
extensions: pdo, pdo_sqlsrv
mssql: server:2017-latest
- php: 7.4
extensions: pdo, pdo_sqlsrv
mssql: server:2019-latest
- php: 8.0
extensions: pdo, pdo_sqlsrv
mssql: server:2017-latest
- php: 8.0
extensions: pdo, pdo_sqlsrv
mssql: server:2019-latest
- php: 8.1
mssql: server:2019-latest
- php: 8.2
mssql: server:2022-latest
services:
mssql:
@ -62,42 +51,28 @@ jobs:
- name: Install PHP with extensions
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.extensions }}
coverage: xdebug
extensions: ${{ env.EXTENSIONS }}
ini-values: date.timezone='UTC'
php-version: ${{ matrix.php }}
tools: composer:v2, pecl
- name: Determine composer cache directory on Linux
run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
- name: Cache dependencies installed with composer
uses: actions/cache@v3
with:
path: ${{ env.COMPOSER_CACHE_DIR }}
key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: |
php${{ matrix.php }}-composer-
- name: Update composer
run: composer self-update
- name: Install dependencies with composer
run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
- name: Install dependencies with composer php 8.0
if: matrix.php == '8.0'
run: composer update --ignore-platform-reqs --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
- name: Run MSSQL tests with PHPUnit and generate coverage.
if: matrix.php == '7.4'
run: vendor/bin/phpunit --group mssql --coverage-clover=coverage.xml --colors=always
- name: PHP Unit tests for PHP 7.1
run: vendor/bin/phpunit --coverage-clover=coverage.clover --group mssql --colors=always
if: matrix.php == '7.1'
- name: Run tests with phpunit without coverage
- name: Run MSSQL tests with PHPUnit.
if: matrix.php > '7.4'
run: vendor/bin/phpunit --group mssql --colors=always
- name: Code coverage
run: |
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover coverage.clover
if: matrix.php == '7.1'
continue-on-error: true # if is fork
- name: Upload coverage to Codecov.
if: matrix.php == '7.4'
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml

View File

@ -4,12 +4,16 @@ on:
name: ci-mysql
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: PHP ${{ matrix.php-version }}-mysql-${{ matrix.mysql-version }}
name: PHP ${{ matrix.php }}-mysql-${{ matrix.mysql }}
env:
extensions: curl, intl, pdo, pdo_mysql
key: cache-v1
XDEBUG_MODE: coverage, develop
runs-on: ${{ matrix.os }}
@ -18,15 +22,19 @@ jobs:
os:
- ubuntu-latest
php-version:
php:
- 7.4
- 8.0
- 8.1
- 8.2
mysql-version:
mysql:
- 5.7
- latest
services:
mysql:
image: mysql:${{ matrix.mysql-version }}
image: mysql:${{ matrix.mysql }}
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: yiitest
@ -35,46 +43,31 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Checkout
- name: Checkout.
uses: actions/checkout@v3
- name: Setup cache environment
id: cache-env
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php-version }}
extensions: ${{ env.extensions }}
key: ${{ env.key }}
- name: Cache extensions
uses: actions/cache@v3
with:
path: ${{ steps.cache-env.outputs.dir }}
key: ${{ steps.cache-env.outputs.key }}
restore-keys: ${{ steps.cache-env.outputs.key }}
- name: Install PHP with extensions
- name: Install PHP with extensions.
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: ${{ env.extensions }}
coverage: xdebug
extensions: ${{ env.EXTENSIONS }}
ini-values: date.timezone='UTC'
coverage: pcov
php-version: ${{ matrix.php }}
tools: composer:v2, pecl
- name: Determine composer cache directory
if: matrix.os == 'ubuntu-latest'
run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
- name: Cache dependencies installed with composer
uses: actions/cache@v1
with:
path: ${{ env.COMPOSER_CACHE_DIR }}
key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }}
restore-keys: |
php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-
- name: Install dependencies with composer
- name: Install dependencies with composer.
run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
- name: Run mysql tests with phpunit
- name: Run MySQL tests with PHPUnit and generate coverage.
if: matrix.php == '7.4'
run: vendor/bin/phpunit --group mysql --coverage-clover=coverage.xml --colors=always
- name: Run MySQL tests with PHPUnit.
if: matrix.php > '7.4'
run: vendor/bin/phpunit --group mysql --colors=always
- name: Upload coverage to Codecov.
if: matrix.php == '7.4'
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml

36
.github/workflows/ci-node.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: build-node
on: [push, pull_request]
env:
DEFAULT_COMPOSER_FLAGS: "--prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: NPM 6 on ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Checkout.
uses: actions/checkout@v3
- name: Install dependencies.
run: composer update $DEFAULT_COMPOSER_FLAGS
- name: Install JQuery `3.6.*@stable` for tests.
run: composer require "bower-asset/jquery:3.6.*@stable"
- name: Install node.js.
uses: actions/setup-node@v1
with:
node-version: 6
- name: Tests.
run: |
npm install
npm test

View File

@ -4,13 +4,17 @@ on:
name: ci-oracle
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: PHP ${{ matrix.php }}-${{ matrix.os }}
env:
extensions: oci8, pdo, pdo_oci
key: cache-v1
XDEBUG_MODE: coverage, develop
runs-on: ${{ matrix.os }}
@ -30,52 +34,28 @@ jobs:
options: --name=oci
steps:
- name: Checkout
- name: Checkout.
uses: actions/checkout@v3
- name: Setup cache environment
id: cache-env
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php }}
extensions: ${{ env.extensions }}
key: ${{ env.key }}
- name: Cache extensions
uses: actions/cache@v1
with:
path: ${{ steps.cache-env.outputs.dir }}
key: ${{ steps.cache-env.outputs.key }}
restore-keys: ${{ steps.cache-env.outputs.key }}
- name: Install PHP with extensions
- name: Install PHP with extensions.
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ env.extensions }}
coverage: xdebug
extensions: ${{ env.EXTENSIONS }}
ini-values: date.timezone='UTC'
coverage: pcov
php-version: ${{ matrix.php }}
tools: composer:v2, pecl
- name: Determine composer cache directory
run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
- name: Update composer.
run: composer self-update
- name: Cache dependencies installed with composer
uses: actions/cache@v3
with:
path: ${{ env.COMPOSER_CACHE_DIR }}
key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: |
php${{ matrix.php }}-composer-
- name: Install dependencies with composer
- name: Install dependencies with composer.
run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
- name: PHP Unit tests
run: vendor/bin/phpunit --coverage-clover=coverage.clover --group oci --colors=always
- name: Run Oracle tests with PHPUnit and generate coverage.
run: vendor/bin/phpunit --group oci --coverage-clover=coverage.xml --colors=always
- name: Code coverage
run: |
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover coverage.clover
continue-on-error: true # if is fork
- name: Upload coverage to Codecov.
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml

View File

@ -4,12 +4,16 @@ on:
name: ci-pgsql
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: PHP ${{ matrix.php-version }}-pgsql-${{ matrix.pgsql-version }}
name: PHP ${{ matrix.php }}-pgsql-${{ matrix.pgsql }}
env:
extensions: curl, intl, pdo, pdo_pgsql
key: cache-v1
XDEBUG_MODE: coverage, develop
runs-on: ${{ matrix.os }}
@ -18,19 +22,22 @@ jobs:
os:
- ubuntu-latest
php-version:
php:
- 7.4
- 8.0
- 8.1
pgsql-version:
pgsql:
- 10
- 11
- 12
- 13
- 14
- 15
services:
postgres:
image: postgres:${{ matrix.pgsql-version }}
image: postgres:${{ matrix.pgsql }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@ -40,46 +47,34 @@ jobs:
options: --name=postgres --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Checkout
- name: Checkout.
uses: actions/checkout@v3
- name: Setup cache environment
id: cache-env
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php-version }}
extensions: ${{ env.extensions }}
key: ${{ env.key }}
- name: Cache extensions
uses: actions/cache@v1
with:
path: ${{ steps.cache-env.outputs.dir }}
key: ${{ steps.cache-env.outputs.key }}
restore-keys: ${{ steps.cache-env.outputs.key }}
- name: Install PHP with extensions
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: ${{ env.extensions }}
coverage: xdebug
extensions: ${{ env.EXTENSIONS }}
ini-values: date.timezone='UTC'
coverage: pcov
php-version: ${{ matrix.php }}
tools: composer:v2, pecl
- name: Determine composer cache directory
if: matrix.os == 'ubuntu-latest'
run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
- name: Update composer.
run: composer self-update
- name: Cache dependencies installed with composer
uses: actions/cache@v3
with:
path: ${{ env.COMPOSER_CACHE_DIR }}
key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }}
restore-keys: |
php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-
- name: Install dependencies with composer
- name: Install dependencies with composer.
run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
- name: Run pgsql tests with phpunit
- name: Run Pgsql tests with PHPUnit and generate coverage.
if: matrix.php == '7.4'
run: vendor/bin/phpunit --group pgsql --coverage-clover=coverage.xml --colors=always
- name: Run Pgsql tests with PHPUnit.
if: matrix.php > '7.4'
run: vendor/bin/phpunit --group pgsql --colors=always
- name: Upload coverage to Codecov.
if: matrix.php == '7.4'
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml

63
.github/workflows/ci-sqlite.yml vendored Normal file
View File

@ -0,0 +1,63 @@
on:
- pull_request
- push
name: ci-sqlite
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: PHP ${{ matrix.php }}-sqlite
env:
EXTENSIONS: pdo, pdo_sqlite, sqlite3
XDEBUG_MODE: coverage, develop
runs-on: ubuntu-latest
strategy:
matrix:
os:
- ubuntu-latest
php:
- 7.4
- 8.0
- 8.1
- 8.2
steps:
- name: Checkout.
uses: actions/checkout@v3
- name: Install PHP with extensions.
uses: shivammathur/setup-php@v2
with:
coverage: xdebug
extensions: ${{ env.EXTENSIONS }}
ini-values: date.timezone='UTC'
php-version: ${{ matrix.php }}
tools: composer:v2, pecl
- name: Update composer.
run: composer self-update
- name: Install dependencies with composer.
run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
- name: Run SQLite tests with PHPUnit and generate coverage.
if: matrix.php == '7.4'
run: vendor/bin/phpunit --group sqlite --coverage-clover=coverage.xml --colors=always
- name: Run SQLite tests with PHPUnit.
if: matrix.php > '7.4'
run: vendor/bin/phpunit --group sqlite --colors=always
- name: Upload coverage to Codecov.
if: matrix.php == '7.4'
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml

View File

@ -4,10 +4,7 @@ FROM ${DOCKER_YII2_PHP_IMAGE}
# Project source-code
WORKDIR /project
ADD composer.* /project/
# Apply testing patches
ADD tests/phpunit_mock_objects.patch /project/tests/phpunit_mock_objects.patch
ADD tests/phpunit_getopt.patch /project/tests/phpunit_getopt.patch
# Install packgaes
# Install packages
RUN /usr/local/bin/composer install --prefer-dist
ADD ./ /project
ENV PATH /project/vendor/bin:${PATH}

View File

@ -49,14 +49,19 @@ class MimeTypeController extends Controller
* @var array MIME types to add to the ones parsed from Apache files
*/
private $additionalMimeTypes = [
'apng' => 'image/apng',
'avif' => 'image/avif',
'jfif' => 'image/jpeg',
'mjs' => 'text/javascript',
'pjp' => 'image/jpeg',
'pjpeg' => 'image/jpeg',
];
/**
* @param string $outFile the mime file to update. Defaults to @yii/helpers/mimeTypes.php
* @param string $aliasesOutFile the aliases file to update. Defaults to @yii/helpers/mimeAliases.php
*/
public function actionIndex($outFile = null, $aliasesOutFile = null)
public function actionIndex($outFile = null, $aliasesOutFile = null, $extensionsOutFile = null)
{
if ($outFile === null) {
$outFile = Yii::getAlias('@yii/helpers/mimeTypes.php');
@ -66,11 +71,16 @@ class MimeTypeController extends Controller
$aliasesOutFile = Yii::getAlias('@yii/helpers/mimeAliases.php');
}
if ($extensionsOutFile === null) {
$extensionsOutFile = Yii::getAlias('@yii/helpers/mimeExtensions.php');
}
$this->stdout('Downloading mime-type file from apache httpd repository...');
if ($apacheMimeTypesFileContent = file_get_contents('https://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=co')) {
$this->stdout("Done.\n", Console::FG_GREEN);
$this->generateMimeTypesFile($outFile, $apacheMimeTypesFileContent);
$this->generateMimeAliasesFile($aliasesOutFile);
$this->generateMimeExtensionsFile($extensionsOutFile, $apacheMimeTypesFileContent);
} else {
$this->stderr("Failed to download mime.types file from apache SVN.\n");
}
@ -97,8 +107,8 @@ class MimeTypeController extends Controller
}
}
}
$mimeMap = array_merge($mimeMap, $this->additionalMimeTypes);
ksort($mimeMap);
$mimeMap = array_replace($mimeMap, $this->additionalMimeTypes);
ksort($mimeMap, SORT_STRING);
$array = VarDumper::export($mimeMap);
$content = <<<EOD
@ -119,6 +129,7 @@ if (PHP_VERSION_ID >= 80100) {
}
return \$mimeTypes;
EOD;
file_put_contents($outFile, $content);
$this->stdout("done.\n", Console::FG_GREEN);
@ -140,6 +151,67 @@ EOD;
*/
return $array;
EOD;
file_put_contents($outFile, $content);
$this->stdout("done.\n", Console::FG_GREEN);
}
/**
* @param string $outFile
* @param string $content
*/
private function generateMimeExtensionsFile($outFile, $content)
{
$this->stdout("Generating file $outFile...");
$extensionMap = [];
foreach (explode("\n", $content) as $line) {
$line = trim($line);
if (empty($line) || strpos($line, '#') === 0) { // skip comments and empty lines
continue;
}
$parts = preg_split('/\s+/', $line);
$mime = array_shift($parts);
if (!empty($parts)) {
$extensionMap[$mime] = [];
foreach ($parts as $ext) {
if (!empty($ext)) {
$extensionMap[$mime][] = $ext;
}
}
}
}
foreach ($this->additionalMimeTypes as $ext => $mime) {
if (!array_key_exists($mime, $extensionMap)) {
$extensionMap[$mime] = [];
}
$extensionMap[$mime][] = $ext;
}
foreach ($extensionMap as $mime => $extensions) {
if (count($extensions) === 1) {
$extensionMap[$mime] = $extensions[0];
}
}
ksort($extensionMap, SORT_STRING);
$array = VarDumper::export($extensionMap);
$content = <<<EOD
<?php
/**
* MIME type extensions.
*
* This file contains most commonly used extensions for MIME types.
* If there are multiple extensions for a singe MIME type
* they are ordered from most to least common.
* Its content is generated from the apache http mime.types file.
* https://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?view=markup
* This file has been placed in the public domain for unlimited redistribution.
*/
return $array;
EOD;
file_put_contents($outFile, $content);
$this->stdout("done.\n", Console::FG_GREEN);

View File

@ -218,10 +218,10 @@ class ReleaseController extends Controller
$gitDir = reset($what) === 'framework' ? 'framework/' : '';
$gitVersion = $versions[reset($what)];
if (strncmp('app-', reset($what), 4) !== 0) {
$this->stdout("- no accidentally added CHANGELOG lines for other versions than this one?\n\n git diff $gitVersion.. ${gitDir}CHANGELOG.md\n\n");
$this->stdout("- no accidentally added CHANGELOG lines for other versions than this one?\n\n git diff $gitVersion.. {$gitDir}CHANGELOG.md\n\n");
$this->stdout("- are all new `@since` tags for this release version?\n");
}
$this->stdout("- other issues with code changes?\n\n git diff -w $gitVersion.. ${gitDir}\n\n");
$this->stdout("- other issues with code changes?\n\n git diff -w $gitVersion.. {$gitDir}\n\n");
$travisUrl = reset($what) === 'framework' ? '' : '-' . reset($what);
$this->stdout("- are unit tests passing on travis? https://travis-ci.com/yiisoft/yii2$travisUrl/builds\n");
$this->stdout("- also make sure the milestone on github is complete and no issues or PRs are left open.\n\n");

View File

@ -75,7 +75,7 @@
"yiisoft/yii2-composer": "~2.0.4",
"ezyang/htmlpurifier": "^4.6",
"cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0",
"bower-asset/jquery": "3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable",
"bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable",
"bower-asset/inputmask": "~3.2.2 | ~3.3.5",
"bower-asset/punycode": "1.3.*",
"bower-asset/yii2-pjax": "~2.0.1",

1935
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -117,7 +117,7 @@ Seguridad
* **TBD** [Autenticación](security-authentication.md)
* **TBD** [Autorización](security-authorization.md)
* **TBD** [Trabajar con contraseñas](security-passwords.md)
* **TBD** [Autenticar Clientes](security-auth-clients.md)
* [Autenticar Clientes](https://www.yiiframework.com/extension/yiisoft/yii2-authclient/doc/guide)
* **TBD** [Buenas prácticas](security-best-practices.md)

View File

@ -7,5 +7,6 @@
"Lucas Barros",
"Raphael de Almeida",
"Sidney da Silva Lins",
"Wanderson Bragança"
"Wanderson Bragança",
"Anthony Tesche"
]

View File

@ -0,0 +1,103 @@
Yii e Docker
==============
Para o desenvolvimento e implantação de aplicativos Yii, eles podem ser executados como contêineres Docker. Um contêiner é como uma máquina virtual isolada e leve que mapeia seus serviços para as portas do host, ou seja, um servidor da web em um contêiner na porta 80 está disponível na porta 8888 do seu (local) host.
Os contêineres podem resolver muitos problemas, como ter versões idênticas de software no computador do desenvolvedor e no servidor, implantações rápidas ou simulação de arquitetura multi-servidor durante o desenvolvimento.
Você pode ler mais sobre contêineres Docker em [docker.com](https://www.docker.com/why-docker).
## Requisitos
- `docker`
- `docker-compose`
Visite a [página de download](https://www.docker.com/products/container-runtime) para obter as ferramentas do Docker.
## Instalação
Após a instalação, você deve ser capaz de executar o comando docker ps e ver uma saída semelhante a esta:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
```
Isso significa que o seu daemon Docker está em execução.
Além disso, execute o comando docker-compose version, a saída deve ser semelhante a esta:
```
docker-compose version 1.20.0, build unknown
docker-py version: 3.1.3
CPython version: 3.6.4
OpenSSL version: OpenSSL 1.1.0g 2 Nov 2017
```
Com o Compose, você pode configurar e gerenciar todos os serviços necessários para a sua aplicação, como bancos de dados e cache.
## Recursos
- As imagens base do PHP para Yii podem ser encontradas em [yii2-docker](https://github.com/yiisoft/yii2-docker)
- Suporte do Docker para [yii2-app-basic](https://github.com/yiisoft/yii2-app-basic#install-with-docker)
- Suporte do Docker para [yii2-app-advanced](https://github.com/yiisoft/yii2-app-advanced/pull/347) está em desenvolvimento
## Uso
Os comandos básicos do Docker são
docker-compose up -d
para iniciar todos os serviços em sua pilha, em segundo plano
docker-compose ps
para listar os serviços em execução
docker-compose logs -f
para visualizar os logs de todos os serviços continuamente
docker-compose stop
para interromper todos os serviços em sua pilha de forma elegante
docker-compose kill
para interromper todos os serviços em sua pilha imediatamente
docker-compose down -v
para parar e remover todos os serviços, **atenção à perda de dados ao não usar volumes do host**
Para executar comandos em um contêiner:
docker-compose run --rm php composer install
executa a instalação do Composer em um novo contêiner
docker-compose exec php bash
executa um shell bash em um serviço php que está em *execução*.
## Tópicos avançados
### Testes do framework Yii
Você pode executar os testes do framework Yii em um contêiner Docker, conforme descrito [aqui](https://github.com/yiisoft/yii2/blob/master/tests/README.md#dockerized-testing).
### Database administration tools
Ao executar o MySQL como (`mysql`), você pode adicionar um contêiner do phpMyAdmin à sua pilha, como mostrado abaixo:
```
phpmyadmin:
image: phpmyadmin/phpmyadmin
ports:
- '8888:80'
environment:
- PMA_ARBITRARY=1
- PMA_HOST=mysql
depends_on:
- mysql
```

View File

@ -373,7 +373,7 @@ $query->orderBy([
```
В данном коде, ключи массива - это имена столбцов, а значения массива - это соответствующее направление сортировки.
PHP константа `SORT_ASC` определяет сортировку по возрастанию и `SORT_DESC` сортировка по умолчанию.
PHP константа `SORT_ASC` определяет сортировку по возрастанию и `SORT_DESC` сортировку по убыванию.
Если `ORDER BY` содержит только простые имена столбцов, вы можете определить их с помощью столбцов, также
как и при написании обычного SQL. Например,

View File

@ -10,7 +10,7 @@
В конструкторе приложения происходит следующий процесс предзагрузки:
1. Вызывается метод [[yii\base\Application::preInit()|preInit()]], которые конфигурирует свойства приложения, имеющие
1. Вызывается метод [[yii\base\Application::preInit()|preInit()]], который конфигурирует свойства приложения, имеющие
наивысший приоритет, такие как [[yii\base\Application::basePath|basePath]];
2. Регистрируется [[yii\base\Application::errorHandler|обработчик ошибок]];
3. Происходит инициализация свойств приложения согласно заданной конфигурации;

View File

@ -82,6 +82,8 @@ and accessible. The following code shows how to configure the `cache` applicatio
You can then access the above cache component using the expression `Yii::$app->cache`.
If no cache component is specified, then Yii will use [yii\caching\FileCache](https://www.yiiframework.com/doc/api/2.0/yii-caching-filecache) as default.
Because all cache components support the same set of APIs, you can swap the underlying cache component
with a different one by reconfiguring it in the application configuration without modifying the code that uses the cache.
For example, you can modify the above configuration to use [[yii\caching\ApcCache|APC cache]]:

View File

@ -263,6 +263,12 @@ Further reading on the topic:
- <https://owasp.org/www-community/SameSite>
Avoiding arbitrary object instantiations
----------------------------------------
Yii [configurations](concept-configurations.md) are associative arrays used by the framework to instantiate new objects through `Yii::createObject($config)`. These arrays specify the class name for instantiation, and it is important to ensure that this class name does not originate from untrusted sources. Otherwise, it can lead to Unsafe Reflection, a vulnerability that allows the execution of malicious code by exploiting the loading of specific classes. Additionally, when you need to dynamically add keys to an object derived from a framework class, such as the base `Component` class, it's essential to validate these dynamic properties using a whitelist approach. This precaution is necessary because the framework might employ `Yii::createObject($config)` within the `__set()` magic method.
Avoiding file exposure
----------------------

View File

@ -293,6 +293,8 @@ public function rules()
// username and password are required in "login" scenario
[['username', 'password'], 'required', 'on' => self::SCENARIO_LOGIN],
[['username'], 'string'], // username must always be a string, this rule applies to all scenarios
];
}
```

View File

@ -68,7 +68,7 @@ class BaseYii
*/
public static $classMap = [];
/**
* @var \yii\console\Application|\yii\web\Application|\yii\base\Application the application instance
* @var \yii\console\Application|\yii\web\Application the application instance
*/
public static $app;
/**
@ -93,7 +93,7 @@ class BaseYii
*/
public static function getVersion()
{
return '2.0.48-dev';
return '2.0.50-dev';
}
/**

View File

@ -1,27 +1,77 @@
Yii Framework 2 Change Log
==========================
2.0.48 under development
2.0.50 under development
------------------------
- Enh #19766: Add support for PHP generators to JSON helper (vladis84)
- Bug #19683: Updated `framework\mimeType.php` to the actual value. Fix typo in `build/controllers/MimeTypeController.php` (DeryabinSergey)
- Bug #19705: Add binary and other data type to `$typeMap` list for MySQL (sohelahmed7)
- Enh #19741: Added option to use a closure for `$variations` definition in `yii\filters\PageCache` (nadar)
- Bug #19925: Improved PHP version check when handling MIME types (schmunk42)
- Bug #19940: File Log writer without newline (terabytesoftw)
- Bug #19951: Removed unneeded MIME file tests (schmunk42)
- Bug #19950: Fix `Query::groupBy(null)` causes error for PHP 8.1: `trim(): Passing null to parameter #1 ($string) of type string is deprecated` (uaoleg)
2.0.49 August 29, 2023
----------------------
- Bug #9899: Fix caching a MSSQL query with BLOB data type (terabytesoftw)
- Bug #16208: Fix `yii\log\FileTarget` to not export empty messages (terabytesoftw)
- Bug #18859: Fix `yii\web\Controller::bindInjectedParams()` to not throw error when argument of `ReflectionUnionType` type is passed (bizley)
- Bug #19857: Fix AttributeTypecastBehavior::resetOldAttributes() causes "class has no attribute named" InvalidArgumentException (uaoleg)
- Bug #19868: Added whitespace sanitation for tests, due to updates in ICU 72 (schmunk42)
- Bug #19872: Fixed the definition of dirty attributes in AR properties for a non-associative array in case of changing the order of elements (eegusakov)
- Bug #19899: Fixed `GridView` in some cases calling `Model::generateAttributeLabel()` to generate label values that are never used (PowerGamer1)
- Bug #19906: Fixed multiline strings in the `\yii\console\widgets\Table` widget (rhertogh)
- Bug #19908: Fix associative array cell content rendering in Table widget (rhertogh)
- Bug #19911: Resolved inconsistency in `ActiveRecord::getAttributeLabel()` with regard of overriding in primary model labels for attributes of related model in favor of allowing such overriding for all levels of relation nesting (PowerGamer1)
- Bug #19914: Fixed `ArrayHelper::keyExists()` and `::remove()` functions when the key is a float and the value is `null` (rhertogh)
- Bug #19924: Fix `yii\i18n\Formatter` to not throw error `Unknown named parameter` under PHP 8 (arollmann)
- Enh #19841: Allow jQuery 3.7 to be installed (wouter90)
- Enh #19853: Added support for default value for `\yii\helpers\Console::select()` (rhertogh)
- Enh #19884: Added support Enums in Query Builder (sk1t0n)
- Enh #19920: Broadened the accepted type of `Cookie::$expire` from `int` to `int|string|\DateTimeInterface|null` (rhertogh)
2.0.48.1 May 24, 2023
---------------------
- Bug #19847: Fix regression introduced in #15376 that caused `DbManager::getRolesByUser()` to return stale data (michaelarnauts)
2.0.48 May 22, 2023
-------------------
- Bug #15376: Added $userId for RBAC roles cache (manchenkoff)
- Bug #17194: Fix unnecessary SQL updates in the database on attributes typecast via `yii\behaviors\AttributeTypecastBehavior` (aivchen)
- Bug #18867: Fixed multiple issues with `yii\grid\CheckboxColumn`: "check all" checkbox not being checked on page load when all data row checkboxes are initially checked; clicking checkboxes triggered "change" event for other checkboxes that do not change their state; "check all" checkbox not being checked when disabled checkboxes are present and clicking last non-checked data row checkbox (PowerGamer1)
- Bug #19635: PHP 8.2 compatibility fix for `yii\validators\DateValidator` (PowerGamer1)
- Bug #17194: Fix unnecessary SQL updates in the database on attributes typecast via `yii\behaviors\AttributeTypecastBehavior` (aivchen)
- Bug #19683: Updated `framework\mimeType.php` to the actual value. Fix typo in `build/controllers/MimeTypeController.php` (DeryabinSergey)
- Bug #19693: Fix db/Command not caching `NULL` result with scalar fetchMode (Arkeins)
- Enh #15376: Added cache usage for `yii\rbac\DbManager::getRolesByUser()` (manchenkoff)
- Enh #9740: Usage of DI instead of new keyword in Schemas (manchenkoff)
- Enh #19689: Remove empty elements from the `class` array in `yii\helpers\BaseHtml::renderTagAttributes()` to prevent unwanted spaces (MoritzLost)
- Chg #19696: Change visibility of `yii\web\View::isPageEnded` to `protected` (lubosdz, samdark)
- Bug #19705: Add binary and other data types to `$typeMap` list for MySQL (sohelahmed7)
- Bug #19712: Cast shell_exec() output to string for jsCompressor (impayru)
- Bug #19720: Fix "zh-HK" locale causing [error][yii\i18n\PhpMessageSource::loadFallbackMessages] The message file for category 'yii' doesn't exist (uaoleg)
- Bug #19731: Fix `yii\data\Sort` to generate a proper link when multisort is on and attribute has a default sort order set (bizley)
- Bug #19734: PHP 8.1 compatibility fix for `$query->orderBy(null)` (uaoleg)
- Bug #19731: Fix `yii\data\Sort` to generate proper link when multisort is on and attribute has a default sort order set (bizley)
- Bug #19735: Fix `yii\validators\NumberValidator` to use programmable message for the value validation (bizley)
- Bug #19735: Fix `yii\validators\NumberValidator` to use a programmable message for the value validation (bizley)
- Bug #19736: Fix `StringHelper::truncate(null, 10)` causes error `Deprecated: mb_strlen(): Passing null to parameter #1 ($string) of type string is deprecated` (uaoleg)
- Bug #19743: Non-associative array values in AR weren't considered dirty when reordered (samdark)
- Bug #19749: Add PHP 8.2 support (samdark, schmunk42, aldok10, DanaLuther)
- Bug #19770: Fix `yii\mutex\MysqlMutex` `keyPrefix` expression param binding (kamarton)
- Bug #19795: Fix `yii\web\Response::redirect()` to prevent setting headers with URL containing new line character (bizley)
- Bug #19807: Fix REST serializer not using `serializeModel()` when working with an array of models (zucha)
- Bug #19813: Fix `yii\base\DynamicModel` validation with validators that reference missing attributes (michaelarnauts)
- Bug #19828: Fix "strtr(): Passing null to parameter #1 ($string) of type string is deprecated" (uaoleg)
- Bug #19837: Fixed processing of numeric file extensions in `yii\build\controllers\MimeTypeController::generateMimeTypesFile()` (rhertogh)
- Enh #9740: Usage of DI instead of new keyword in Schemas (manchenkoff)
- Enh #15376: Added cache usage for `yii\rbac\DbManager::getRolesByUser()` (manchenkoff)
- Enh #19689: Remove empty elements from the `class` array in `yii\helpers\BaseHtml::renderTagAttributes()` to prevent unwanted spaces (MoritzLost)
- Enh #19741: Added option to use a closure for `$variations` definition in `yii\filters\PageCache` (nadar)
- Enh #19766: Add support for PHP generators to JSON helper (vladis84)
- Enh #19794: Add caching in `yii\web\Request` for `getUserIP()` and `getSecureForwardedHeaderTrustedParts()` (rhertogh)
- Enh #19804: Remove the unnecessary call to `$this->oldAttributes` in `BaseActiveRecord::getDirtyAttributes()` (thiagotalma)
- Enh #19816: Explicitly pass `$fallbackToMaster` as `true` to `getSlavePdo()` to ensure it isn't affected by child class with changed defaults (developedsoftware)
- Enh #19838: Added `yii\helpers\BaseFileHelper::getExtensionByMimeType()` to get the most common extension for a given MIME type (rhertogh)
- Chg #19696: Change visibility of `yii\web\View::isPageEnded` to `protected` (lubosdz, samdark)
2.0.47 November 18, 2022
------------------------

View File

@ -52,6 +52,17 @@ version B between A and C, you need to follow the instructions
for both A and B.
Upgrade from Yii 2.0.48
-----------------------
* Since Yii 2.0.49 the `yii\console\Controller::select()` function supports a default value and respects
the `yii\console\Controller::$interactive` setting. Before the user was always prompted to select an option
regardless of the `$interactive` setting. Now the `$default` value is automatically returned when `$interactive` is
`false`.
* The function signature for `yii\console\Controller::select()` and `yii\helpers\BaseConsole::select()` have changed.
They now have an additional `$default = null` parameter. In case those methods are overwritten you will need to
update your child classes accordingly.
Upgrade from Yii 2.0.46
-----------------------
@ -89,6 +100,11 @@ Upgrade from Yii 2.0.45
2.0.45 behavior, [introduce your own method](https://github.com/yiisoft/yii2/pull/19495/files).
* `yii\log\FileTarget::$rotateByCopy` is now deprecated and setting it to `false` has no effect since rotating of
the files is done only by copy.
* `yii\validators\UniqueValidator` and `yii\validators\ExistValidator`, when used on multiple attributes, now only
generate an error on a single attribute. Previously, they would report a separate error on each attribute.
Old behavior can be achieved by setting `'skipOnError' => false`, but this might have undesired side effects with
additional validators on one of the target attributes.
See [issue #19407](https://github.com/yiisoft/yii2/issues/19407)
Upgrade from Yii 2.0.44
-----------------------

View File

@ -15,7 +15,7 @@ namespace yii\base;
* like the following:
*
* ```php
* public function __constructor($param1, $param2, ..., $config = [])
* public function __construct($param1, $param2, ..., $config = [])
* ```
*
* That is, the last parameter of the constructor must accept a configuration array.

View File

@ -201,6 +201,7 @@ class DynamicModel extends Model
}
$validators->append($validator);
$this->defineAttributesByValidator($validator);
return $this;
}
@ -223,9 +224,11 @@ class DynamicModel extends Model
foreach ($rules as $rule) {
if ($rule instanceof Validator) {
$validators->append($rule);
$model->defineAttributesByValidator($rule);
} elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type
$validator = Validator::createValidator($rule[1], $model, (array)$rule[0], array_slice($rule, 2));
$validators->append($validator);
$model->defineAttributesByValidator($validator);
} else {
throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.');
}
@ -237,6 +240,19 @@ class DynamicModel extends Model
return $model;
}
/**
* Define the attributes that applies to the specified Validator.
* @param Validator $validator the validator whose attributes are to be defined.
*/
private function defineAttributesByValidator($validator)
{
foreach ($validator->getAttributeNames() as $attribute) {
if (!$this->hasAttribute($attribute)) {
$this->defineAttribute($attribute);
}
}
}
/**
* {@inheritdoc}
*/

View File

@ -366,7 +366,10 @@ class AttributeTypecastBehavior extends Behavior
$this->resetOldAttributes();
}
private function resetOldAttributes()
/**
* Resets the old values of the named attributes.
*/
protected function resetOldAttributes()
{
if ($this->attributeTypes === null) {
return;
@ -375,7 +378,9 @@ class AttributeTypecastBehavior extends Behavior
$attributes = array_keys($this->attributeTypes);
foreach ($attributes as $attribute) {
$this->owner->setOldAttribute($attribute, $this->owner->{$attribute});
if ($this->owner->canSetOldAttribute($attribute)) {
$this->owner->setOldAttribute($attribute, $this->owner->{$attribute});
}
}
}
}

View File

@ -317,7 +317,7 @@ class DbCache extends Cache
*/
protected function getDataFieldName()
{
return $this->isVarbinaryDataField() ? 'convert(nvarchar(max),[data]) data' : 'data';
return $this->isVarbinaryDataField() ? 'CONVERT(VARCHAR(MAX), [[data]]) data' : 'data';
}
/**

View File

@ -345,9 +345,9 @@ return [
'yii\validators\RequiredValidator' => YII2_PATH . '/validators/RequiredValidator.php',
'yii\validators\SafeValidator' => YII2_PATH . '/validators/SafeValidator.php',
'yii\validators\StringValidator' => YII2_PATH . '/validators/StringValidator.php',
'yii\validators\TrimValidator' => YII2_PATH . '/validators/TrimValidator.php',
'yii\validators\UniqueValidator' => YII2_PATH . '/validators/UniqueValidator.php',
'yii\validators\UrlValidator' => YII2_PATH . '/validators/UrlValidator.php',
'yii\validators\TrimValidator' => YII2_PATH . '/validators/TrimValidator.php',
'yii\validators\ValidationAsset' => YII2_PATH . '/validators/ValidationAsset.php',
'yii\validators\Validator' => YII2_PATH . '/validators/Validator.php',
'yii\web\Application' => YII2_PATH . '/web/Application.php',

View File

@ -68,9 +68,9 @@
"ext-ctype": "*",
"lib-pcre": "*",
"yiisoft/yii2-composer": "~2.0.4",
"ezyang/htmlpurifier": "~4.6",
"ezyang/htmlpurifier": "^4.6",
"cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0",
"bower-asset/jquery": "3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable",
"bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable",
"bower-asset/inputmask": "~3.2.2 | ~3.3.5",
"bower-asset/punycode": "1.3.*",
"bower-asset/yii2-pjax": "~2.0.1",

View File

@ -28,8 +28,8 @@ use yii\helpers\Inflector;
* where `<route>` is a route to a controller action and the params will be populated as properties of a command.
* See [[options()]] for details.
*
* @property-read string $help The help information for this controller.
* @property-read string $helpSummary The one-line short summary describing this controller.
* @property-read string $help
* @property-read string $helpSummary
* @property-read array $passedOptionValues The properties corresponding to the passed options.
* @property-read array $passedOptions The names of the options passed during execution.
*
@ -400,12 +400,19 @@ class Controller extends \yii\base\Controller
*
* @param string $prompt the prompt message
* @param array $options Key-value array of options to choose from
* @param string|null $default value to use when the user doesn't provide an option.
* If the default is `null`, the user is required to select an option.
*
* @return string An option character the user chose
* @since 2.0.49 Added the $default argument
*/
public function select($prompt, $options = [])
public function select($prompt, $options = [], $default = null)
{
return Console::select($prompt, $options);
if ($this->interactive) {
return Console::select($prompt, $options, $default);
}
return $default;
}
/**

View File

@ -136,7 +136,11 @@ class Table extends Widget
{
$this->rows = array_map(function($row) {
return array_map(function($value) {
return empty($value) && !is_numeric($value) ? ' ' : $value;
return empty($value) && !is_numeric($value)
? ' '
: (is_array($value)
? array_values($value)
: $value);
}, array_values($row));
}, $rows);
return $this;
@ -252,18 +256,32 @@ class Table extends Widget
if ($index !== 0) {
$buffer .= $spanMiddle . ' ';
}
$arrayFromMultilineString = false;
if (is_string($cell)) {
$cellLines = explode(PHP_EOL, $cell);
if (count($cellLines) > 1) {
$cell = $cellLines;
$arrayFromMultilineString = true;
}
}
if (is_array($cell)) {
if (empty($renderedChunkTexts[$index])) {
$renderedChunkTexts[$index] = '';
$start = 0;
$prefix = $this->listPrefix;
$prefix = $arrayFromMultilineString ? '' : $this->listPrefix;
if (!isset($arrayPointer[$index])) {
$arrayPointer[$index] = 0;
}
} else {
$start = mb_strwidth($renderedChunkTexts[$index], Yii::$app->charset);
}
$chunk = Console::ansiColorizedSubstr($cell[$arrayPointer[$index]], $start, $cellSize - 4);
$chunk = Console::ansiColorizedSubstr(
$cell[$arrayPointer[$index]],
$start,
$cellSize - 2 - Console::ansiStrwidth($prefix)
);
$renderedChunkTexts[$index] .= Console::stripAnsiFormat($chunk);
$fullChunkText = Console::stripAnsiFormat($cell[$arrayPointer[$index]]);
if (isset($cell[$arrayPointer[$index] + 1]) && $renderedChunkTexts[$index] === $fullChunkText) {
@ -339,6 +357,9 @@ class Table extends Widget
if (is_array($val)) {
return max(array_map('yii\helpers\Console::ansiStrwidth', $val)) + Console::ansiStrwidth($this->listPrefix);
}
if (is_string($val)) {
return max(array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val)));
}
return Console::ansiStrwidth($val);
}, $column)) + 2;
$this->columnWidths[] = $columnWidth;
@ -388,6 +409,9 @@ class Table extends Widget
if (is_array($val)) {
return array_map('yii\helpers\Console::ansiStrwidth', $val);
}
if (is_string($val)) {
return array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val));
}
return Console::ansiStrwidth($val);
}, $row));
return max($rowsPerCell);

View File

@ -183,15 +183,11 @@ class ActiveDataProvider extends BaseDataProvider
$sort->attributes[$attribute] = [
'asc' => [$attribute => SORT_ASC],
'desc' => [$attribute => SORT_DESC],
'label' => $model->getAttributeLabel($attribute),
];
}
} else {
foreach ($sort->attributes as $attribute => $config) {
if (!isset($config['label'])) {
$sort->attributes[$attribute]['label'] = $model->getAttributeLabel($attribute);
}
}
}
if ($sort->modelClass === null) {
$sort->modelClass = $modelClass;
}
}
}

View File

@ -191,6 +191,12 @@ class Sort extends BaseObject
* @since 2.0.33
*/
public $sortFlags = SORT_REGULAR;
/**
* @var string|null the name of the [[\yii\base\Model]]-based class used by the [[link()]] method to retrieve
* attributes' labels. See [[link]] method for details.
* @since 2.0.49
*/
public $modelClass;
/**
@ -363,7 +369,8 @@ class Sort extends BaseObject
* @param array $options additional HTML attributes for the hyperlink tag.
* There is one special attribute `label` which will be used as the label of the hyperlink.
* If this is not set, the label defined in [[attributes]] will be used.
* If no label is defined, [[\yii\helpers\Inflector::camel2words()]] will be called to get a label.
* If no label is defined, it will be retrieved from the instance of [[modelClass]] (if [[modelClass]] is not null)
* or generated from attribute name using [[\yii\helpers\Inflector::camel2words()]].
* Note that it will not be HTML-encoded.
* @return string the generated hyperlink
* @throws InvalidConfigException if the attribute is unknown
@ -388,6 +395,11 @@ class Sort extends BaseObject
} else {
if (isset($this->attributes[$attribute]['label'])) {
$label = $this->attributes[$attribute]['label'];
} elseif ($this->modelClass !== null) {
$modelClass = $this->modelClass;
/** @var \yii\base\Model $model */
$model = $modelClass::instance();
$label = $model->getAttributeLabel($attribute);
} else {
$label = Inflector::camel2words($attribute);
}

View File

@ -144,6 +144,7 @@ interface ActiveRecordInterface extends StaticInstanceInterface
* // Use where() to ignore the default condition
* // SELECT FROM customer WHERE age>30
* $customers = Customer::find()->where('age>30')->all();
* ```
*
* @return ActiveQueryInterface the newly created [[ActiveQueryInterface]] instance.
*/

View File

@ -282,7 +282,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
*/
public function __get($name)
{
if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
if (array_key_exists($name, $this->_attributes)) {
return $this->_attributes[$name];
}
@ -290,7 +290,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
return null;
}
if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
if (array_key_exists($name, $this->_related)) {
return $this->_related[$name];
}
$value = parent::__get($name);
@ -576,13 +576,24 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
*/
public function setOldAttribute($name, $value)
{
if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) {
if ($this->canSetOldAttribute($name)) {
$this->_oldAttributes[$name] = $value;
} else {
throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
}
}
/**
* Returns if the old named attribute can be set.
* @param string $name the attribute name
* @return bool whether the old attribute can be set
* @see setOldAttribute()
*/
public function canSetOldAttribute($name)
{
return (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name));
}
/**
* Marks an attribute dirty.
* This method may be called to force updating a record when calling [[update()]],
@ -639,7 +650,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
}
} else {
foreach ($this->_attributes as $name => $value) {
if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $this->isAttributeDirty($name, $value))) {
if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $this->isValueDifferent($value, $this->_oldAttributes[$name]))) {
$attributes[$name] = $value;
}
}
@ -1599,40 +1610,46 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
/**
* Returns the text label for the specified attribute.
* If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
* The attribute may be specified in a dot format to retrieve the label from related model or allow this model to override the label defined in related model.
* For example, if the attribute is specified as 'relatedModel1.relatedModel2.attr' the function will return the first label definition it can find
* in the following order:
* - the label for 'relatedModel1.relatedModel2.attr' defined in [[attributeLabels()]] of this model;
* - the label for 'relatedModel2.attr' defined in related model represented by relation 'relatedModel1' of this model;
* - the label for 'attr' defined in related model represented by relation 'relatedModel2' of relation 'relatedModel1'.
* If no label definition was found then the value of $this->generateAttributeLabel('relatedModel1.relatedModel2.attr') will be returned.
* @param string $attribute the attribute name
* @return string the attribute label
* @see generateAttributeLabel()
* @see attributeLabels()
* @see generateAttributeLabel()
*/
public function getAttributeLabel($attribute)
{
$labels = $this->attributeLabels();
if (isset($labels[$attribute])) {
return $labels[$attribute];
} elseif (strpos($attribute, '.')) {
$attributeParts = explode('.', $attribute);
$neededAttribute = array_pop($attributeParts);
$relatedModel = $this;
foreach ($attributeParts as $relationName) {
if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
$relatedModel = $relatedModel->$relationName;
} else {
try {
$relation = $relatedModel->getRelation($relationName);
} catch (InvalidParamException $e) {
return $this->generateAttributeLabel($attribute);
}
/* @var $modelClass ActiveRecordInterface */
$modelClass = $relation->modelClass;
$relatedModel = $modelClass::instance();
}
$model = $this;
$modelAttribute = $attribute;
for (;;) {
$labels = $model->attributeLabels();
if (isset($labels[$modelAttribute])) {
return $labels[$modelAttribute];
}
$labels = $relatedModel->attributeLabels();
if (isset($labels[$neededAttribute])) {
return $labels[$neededAttribute];
$parts = explode('.', $modelAttribute, 2);
if (count($parts) < 2) {
break;
}
list ($relationName, $modelAttribute) = $parts;
if ($model->isRelationPopulated($relationName) && $model->$relationName instanceof self) {
$model = $model->$relationName;
} else {
try {
$relation = $model->getRelation($relationName);
} catch (InvalidArgumentException $e) {
break;
}
/* @var $modelClass ActiveRecordInterface */
$modelClass = $relation->modelClass;
$model = $modelClass::instance();
}
}
@ -1756,18 +1773,18 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
}
/**
* @param string $attribute
* @param mixed $value
* @param mixed $newValue
* @param mixed $oldValue
* @return bool
* @since 2.0.48
*/
private function isAttributeDirty($attribute, $value)
private function isValueDifferent($newValue, $oldValue)
{
$old_attribute = $this->oldAttributes[$attribute];
if (is_array($value) && is_array($this->oldAttributes[$attribute])) {
$value = ArrayHelper::recursiveSort($value);
$old_attribute = ArrayHelper::recursiveSort($old_attribute);
if (is_array($newValue) && is_array($oldValue) && ArrayHelper::isAssociative($oldValue)) {
$newValue = ArrayHelper::recursiveSort($newValue);
$oldValue = ArrayHelper::recursiveSort($oldValue);
}
return $value !== $old_attribute;
return $newValue !== $oldValue;
}
}

View File

@ -258,7 +258,7 @@ class Command extends Component
$forRead = false;
}
if ($forRead || $forRead === null && $this->db->getSchema()->isReadQuery($sql)) {
$pdo = $this->db->getSlavePdo();
$pdo = $this->db->getSlavePdo(true);
} else {
$pdo = $this->db->getMasterPdo();
}
@ -377,6 +377,13 @@ class Command extends Component
$this->pendingParams[$name] = [$value->getValue(), $value->getType()];
$this->params[$name] = $value->getValue();
} else {
if (version_compare(PHP_VERSION, '8.1.0') >= 0) {
if ($value instanceof \BackedEnum) {
$value = $value->value;
} elseif ($value instanceof \UnitEnum) {
$value = $value->name;
}
}
$type = $schema->getPdoType($value);
$this->pendingParams[$name] = [$value, $type];
$this->params[$name] = $value;
@ -631,7 +638,8 @@ class Command extends Component
*
* The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'),
* where name stands for a column name which will be properly quoted by the method, and definition
* stands for the column type which can contain an abstract DB type.
* stands for the column type which must contain an abstract DB type.
*
* The method [[QueryBuilder::getColumnType()]] will be called
* to convert the abstract column types to physical ones. For example, `string` will be converted
* as `varchar(255)`, and `string not null` becomes `varchar(255) not null`.
@ -639,6 +647,16 @@ class Command extends Component
* If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly
* inserted into the generated SQL.
*
* Example usage:
* ```php
* Yii::$app->db->createCommand()->createTable('post', [
* 'id' => 'pk',
* 'title' => 'string',
* 'text' => 'text',
* 'column_name double precision null default null',
* ]);
* ```
*
* @param string $table the name of the table to be created. The name will be properly quoted by the method.
* @param array $columns the columns (name => definition) in the new table.
* @param string|null $options additional SQL fragment that will be appended to the generated SQL.

View File

@ -1013,7 +1013,7 @@ class Connection extends Component
if (($pos = strpos((string)$this->dsn, ':')) !== false) {
$this->_driverName = strtolower(substr($this->dsn, 0, $pos));
} else {
$this->_driverName = strtolower($this->getSlavePdo()->getAttribute(PDO::ATTR_DRIVER_NAME));
$this->_driverName = strtolower($this->getSlavePdo(true)->getAttribute(PDO::ATTR_DRIVER_NAME));
}
}

View File

@ -305,13 +305,25 @@ class Migration extends Component implements MigrationInterface
*
* The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'),
* where name stands for a column name which will be properly quoted by the method, and definition
* stands for the column type which can contain an abstract DB type.
* stands for the column type which must contain an abstract DB type.
*
* The [[QueryBuilder::getColumnType()]] method will be invoked to convert any abstract type into a physical one.
*
* If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly
* put into the generated SQL.
*
* Example usage:
* ```php
* class m200000_000000_create_table_fruits extends \yii\db\Migration
* {
* public function safeUp()
* {
* $this->createTable('{{%fruits}}', [
* // ...
* 'column_name double precision null default null',
* ```
*
*
* @param string $table the name of the table to be created. The name will be properly quoted by the method.
* @param array $columns the columns (name => definition) in the new table.
* @param string|null $options additional SQL fragment that will be appended to the generated SQL.

View File

@ -1049,7 +1049,7 @@ PATTERN;
/**
* Sets the GROUP BY part of the query.
* @param string|array|ExpressionInterface $columns the columns to be grouped by.
* @param string|array|ExpressionInterface|null $columns the columns to be grouped by.
* Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
@ -1067,7 +1067,7 @@ PATTERN;
{
if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
} elseif (!is_array($columns) && !is_null($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
$this->groupBy = $columns;

View File

@ -692,7 +692,7 @@ class QueryBuilder extends \yii\base\BaseObject
*
* The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'),
* where name stands for a column name which will be properly quoted by the method, and definition
* stands for the column type which can contain an abstract DB type.
* stands for the column type which must contain an abstract DB type.
* The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one.
*
* If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly
@ -705,6 +705,7 @@ class QueryBuilder extends \yii\base\BaseObject
* 'id' => 'pk',
* 'name' => 'string',
* 'age' => 'integer',
* 'column_name double precision null default null', # definition only example
* ]);
* ```
*

View File

@ -458,7 +458,7 @@ abstract class Schema extends BaseObject
return $str;
}
if (mb_stripos($this->db->dsn, 'odbc:') === false && ($value = $this->db->getSlavePdo()->quote($str)) !== false) {
if (mb_stripos((string)$this->db->dsn, 'odbc:') === false && ($value = $this->db->getSlavePdo(true)->quote($str)) !== false) {
return $value;
}
@ -695,7 +695,7 @@ abstract class Schema extends BaseObject
public function getServerVersion()
{
if ($this->_serverVersion === null) {
$this->_serverVersion = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
$this->_serverVersion = $this->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_SERVER_VERSION);
}
return $this->_serverVersion;
}
@ -809,7 +809,7 @@ abstract class Schema extends BaseObject
*/
protected function normalizePdoRowKeyCase(array $row, $multiple)
{
if ($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_UPPER) {
if ($this->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_UPPER) {
return $row;
}

View File

@ -18,8 +18,8 @@ use yii\base\InvalidArgumentException;
class LikeCondition extends SimpleCondition
{
/**
* @var array|null|false map of chars to their replacements, false if characters should not be escaped
* or either null or empty array if escaping is condition builder responsibility.
* @var array|null|false map of chars to their replacements, `false` if characters should not be escaped
* or either `null` or empty array if escaping is condition builder responsibility.
* By default it's set to `null`.
*/
protected $escapingReplacements;
@ -40,9 +40,10 @@ class LikeCondition extends SimpleCondition
/**
* This method allows to specify how to escape special characters in the value(s).
*
* @param array an array of mappings from the special characters to their escaped counterparts.
* You may use `false` or an empty array to indicate the values are already escaped and no escape
* should be applied. Note that when using an escape mapping (or the third operand is not provided),
* @param array|null|false an array of mappings from the special characters to their escaped counterparts.
* You may use `false` to indicate the values are already escaped and no escape should be applied,
* or either `null` or empty array if escaping is condition builder responsibility.
* Note that when using an escape mapping (or the third operand is not provided),
* the values will be automatically enclosed within a pair of percentage characters.
*/
public function setEscapingReplacements($escapingReplacements)
@ -51,7 +52,7 @@ class LikeCondition extends SimpleCondition
}
/**
* @return array|false
* @return array|null|false
*/
public function getEscapingReplacements()
{

View File

@ -78,7 +78,7 @@ class LikeConditionBuilder implements ExpressionBuilderInterface
if ($value instanceof ExpressionInterface) {
$phName = $this->queryBuilder->buildExpression($value, $params);
} else {
$phName = $this->queryBuilder->bindParam(empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'), $params);
$phName = $this->queryBuilder->bindParam(empty($escape) ? $value : ('%' . strtr((string)$value, $escape) . '%'), $params);
}
$parts[] = "{$column} {$operator} {$phName}{$escapeSql}";
}

View File

@ -92,7 +92,7 @@ class Schema extends \yii\db\Schema implements ConstraintFinderInterface
*/
protected function findTableNames($schema = '')
{
$pdo = $this->db->getSlavePdo();
$pdo = $this->db->getSlavePdo(true);
$tables = $pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE);
$tableNames = [];
foreach ($tables as $table) {
@ -110,7 +110,7 @@ class Schema extends \yii\db\Schema implements ConstraintFinderInterface
*/
protected function loadTableSchema($name)
{
$pdo = $this->db->getSlavePdo();
$pdo = $this->db->getSlavePdo(true);
$tableInfo = $pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE, $name);
@ -159,7 +159,7 @@ class Schema extends \yii\db\Schema implements ConstraintFinderInterface
*/
protected function loadTablePrimaryKey($tableName)
{
$primaryKey = $this->db->getSlavePdo()->cubrid_schema(\PDO::CUBRID_SCH_PRIMARY_KEY, $tableName);
$primaryKey = $this->db->getSlavePdo(true)->cubrid_schema(\PDO::CUBRID_SCH_PRIMARY_KEY, $tableName);
if (empty($primaryKey)) {
return null;
}
@ -183,7 +183,7 @@ class Schema extends \yii\db\Schema implements ConstraintFinderInterface
3 => 'SET NULL',
];
$foreignKeys = $this->db->getSlavePdo()->cubrid_schema(\PDO::CUBRID_SCH_IMPORTED_KEYS, $tableName);
$foreignKeys = $this->db->getSlavePdo(true)->cubrid_schema(\PDO::CUBRID_SCH_IMPORTED_KEYS, $tableName);
$foreignKeys = ArrayHelper::index($foreignKeys, null, 'FK_NAME');
ArrayHelper::multisort($foreignKeys, 'KEY_SEQ', SORT_ASC, SORT_NUMERIC);
$result = [];
@ -386,7 +386,7 @@ class Schema extends \yii\db\Schema implements ConstraintFinderInterface
*/
private function loadTableConstraints($tableName, $returnType)
{
$constraints = $this->db->getSlavePdo()->cubrid_schema(\PDO::CUBRID_SCH_CONSTRAINT, $tableName);
$constraints = $this->db->getSlavePdo(true)->cubrid_schema(\PDO::CUBRID_SCH_CONSTRAINT, $tableName);
$constraints = ArrayHelper::index($constraints, null, ['TYPE', 'NAME']);
ArrayHelper::multisort($constraints, 'KEY_ORDER', SORT_ASC, SORT_NUMERIC);
$result = [

View File

@ -460,10 +460,9 @@ class QueryBuilder extends \yii\db\QueryBuilder
$columnSchemas = $tableSchema->columns;
foreach ($columns as $name => $value) {
// @see https://github.com/yiisoft/yii2/issues/12599
if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && (is_string($value) || $value === null)) {
$phName = $this->bindParam($value, $params);
if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && (is_string($value))) {
// @see https://github.com/yiisoft/yii2/issues/12599
$columns[$name] = new Expression("CONVERT(VARBINARY(MAX), $phName)", $params);
$columns[$name] = new Expression('CONVERT(VARBINARY(MAX), ' . ('0x' . bin2hex($value)) . ')');
}
}
}

View File

@ -410,7 +410,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
$version = $cache ? $cache->get($key) : null;
if (!$version) {
$version = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
$version = $this->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_SERVER_VERSION);
if ($cache) {
$cache->set($key, $version, $this->db->schemaCacheDuration);
}

View File

@ -484,7 +484,7 @@ SQL;
protected function isOldMysql()
{
if ($this->_oldMysql === null) {
$version = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
$version = $this->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_SERVER_VERSION);
$this->_oldMysql = version_compare($version, '5.1', '<=');
}

View File

@ -327,7 +327,12 @@ class BaseArrayHelper
*/
public static function remove(&$array, $key, $default = null)
{
if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) {
// ToDo: This check can be removed when the minimum PHP version is >= 8.1 (Yii2.2)
if (is_float($key)) {
$key = (int)$key;
}
if (is_array($array) && array_key_exists($key, $array)) {
$value = $array[$key];
unset($array[$key]);
@ -608,17 +613,20 @@ class BaseArrayHelper
* Checks if the given array contains the specified key.
* This method enhances the `array_key_exists()` function by supporting case-insensitive
* key comparison.
* @param string $key the key to check
* @param string|int $key the key to check
* @param array|ArrayAccess $array the array with keys to check
* @param bool $caseSensitive whether the key comparison should be case-sensitive
* @return bool whether the array contains the specified key
*/
public static function keyExists($key, $array, $caseSensitive = true)
{
// ToDo: This check can be removed when the minimum PHP version is >= 8.1 (Yii2.2)
if (is_float($key)) {
$key = (int)$key;
}
if ($caseSensitive) {
// Function `isset` checks key faster but skips `null`, `array_key_exists` handles this case
// https://www.php.net/manual/en/function.array-key-exists.php#107786
if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) {
if (is_array($array) && array_key_exists($key, $array)) {
return true;
}
// Cannot use `array_has_key` on Objects for PHP 7.4+, therefore we need to check using [[ArrayAccess::offsetExists()]]

View File

@ -948,13 +948,17 @@ class BaseConsole
* @param string $prompt the prompt message
* @param array $options Key-value array of options to choose from. Key is what is inputed and used, value is
* what's displayed to end user by help command.
* @param string|null $default value to use when the user doesn't provide an option.
* If the default is `null`, the user is required to select an option.
*
* @return string An option character the user chose
* @since 2.0.49 Added the $default argument
*/
public static function select($prompt, $options = [])
public static function select($prompt, $options = [], $default = null)
{
top:
static::stdout("$prompt [" . implode(',', array_keys($options)) . ',?]: ');
static::stdout("$prompt (" . implode(',', array_keys($options)) . ',?)'
. ($default !== null ? '[' . $default . ']' : '') . ': ');
$input = static::stdin();
if ($input === '?') {
foreach ($options as $key => $value) {
@ -962,6 +966,8 @@ class BaseConsole
}
static::output(' ? - Show help');
goto top;
} elseif ($default !== null && $input === '') {
return $default;
} elseif (!array_key_exists($input, $options)) {
goto top;
}

View File

@ -39,6 +39,11 @@ class BaseFileHelper
* @since 2.0.14
*/
public static $mimeAliasesFile = '@yii/helpers/mimeAliases.php';
/**
* @var string the path (or alias) of a PHP file containing extensions per MIME type.
* @since 2.0.48
*/
public static $mimeExtensionsFile = '@yii/helpers/mimeExtensions.php';
/**
@ -213,10 +218,49 @@ class BaseFileHelper
$mimeType = $aliases[$mimeType];
}
// Note: For backwards compatibility the "MimeTypes" file is used.
$mimeTypes = static::loadMimeTypes($magicFile);
return array_keys($mimeTypes, mb_strtolower($mimeType, 'UTF-8'), true);
}
/**
* Determines the most common extension by given MIME type.
* This method will use a local map between MIME types and extension names.
* @param string $mimeType file MIME type.
* @param bool $preferShort return an extension with a maximum of 3 characters.
* @param string|null $magicFile the path (or alias) of the file that contains all available MIME type information.
* If this is not set, the file specified by [[mimeMagicFile]] will be used.
* @return string|null the extensions corresponding to the specified MIME type
* @since 2.0.48
*/
public static function getExtensionByMimeType($mimeType, $preferShort = false, $magicFile = null)
{
$aliases = static::loadMimeAliases(static::$mimeAliasesFile);
if (isset($aliases[$mimeType])) {
$mimeType = $aliases[$mimeType];
}
$mimeExtensions = static::loadMimeExtensions($magicFile);
if (!array_key_exists($mimeType, $mimeExtensions)) {
return null;
}
$extensions = $mimeExtensions[$mimeType];
if (is_array($extensions)) {
if ($preferShort) {
foreach ($extensions as $extension) {
if (mb_strlen($extension, 'UTF-8') <= 3) {
return $extension;
}
}
}
return $extensions[0];
} else {
return $extensions;
}
}
private static $_mimeTypes = [];
/**
@ -260,6 +304,28 @@ class BaseFileHelper
return self::$_mimeAliases[$aliasesFile];
}
private static $_mimeExtensions = [];
/**
* Loads MIME extensions from the specified file.
* @param string|null $extensionsFile the path (or alias) of the file that contains MIME type aliases.
* If this is not set, the file specified by [[mimeAliasesFile]] will be used.
* @return array the mapping from file extensions to MIME types
* @since 2.0.48
*/
protected static function loadMimeExtensions($extensionsFile)
{
if ($extensionsFile === null) {
$extensionsFile = static::$mimeExtensionsFile;
}
$extensionsFile = Yii::getAlias($extensionsFile);
if (!isset(self::$_mimeExtensions[$extensionsFile])) {
self::$_mimeExtensions[$extensionsFile] = require $extensionsFile;
}
return self::$_mimeExtensions[$extensionsFile];
}
/**
* Copies a whole directory as another one.
* The files and sub-directories will also be copied over.

View File

@ -49,7 +49,7 @@ class BaseStringHelper
$length = static::byteLength($string);
}
return mb_substr($string, $start, $length, '8bit');
return mb_substr((string)$string, $start, $length, '8bit');
}
/**
@ -67,6 +67,8 @@ class BaseStringHelper
*/
public static function basename($path, $suffix = '')
{
$path = (string)$path;
$len = mb_strlen($suffix);
if ($len > 0 && mb_substr($path, -$len) === $suffix) {
$path = mb_substr($path, 0, -$len);
@ -93,7 +95,7 @@ class BaseStringHelper
public static function dirname($path)
{
$normalizedPath = rtrim(
str_replace('\\', '/', $path),
str_replace('\\', '/', (string)$path),
'/'
);
$separatorPosition = mb_strrpos($normalizedPath, '/');
@ -122,6 +124,8 @@ class BaseStringHelper
*/
public static function truncate($string, $length, $suffix = '...', $encoding = null, $asHtml = false)
{
$string = (string)$string;
if ($encoding === null) {
$encoding = Yii::$app ? Yii::$app->charset : 'UTF-8';
}
@ -233,6 +237,9 @@ class BaseStringHelper
*/
public static function startsWith($string, $with, $caseSensitive = true)
{
$string = (string)$string;
$with = (string)$with;
if (!$bytes = static::byteLength($with)) {
return true;
}
@ -257,6 +264,9 @@ class BaseStringHelper
*/
public static function endsWith($string, $with, $caseSensitive = true)
{
$string = (string)$string;
$with = (string)$with;
if (!$bytes = static::byteLength($with)) {
return true;
}

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
* This file has been placed in the public domain for unlimited redistribution.
*/
$mimeTypes = [
123 => 'application/vnd.lotus-1-2-3',
'3dml' => 'text/vnd.in3d.3dml',
'3ds' => 'image/x-3ds',
'3g2' => 'video/3gpp2',
@ -37,6 +38,7 @@ $mimeTypes = [
'ait' => 'application/vnd.dvb.ait',
'ami' => 'application/vnd.amiga.ami',
'apk' => 'application/vnd.android.package-archive',
'apng' => 'image/apng',
'appcache' => 'text/cache-manifest',
'application' => 'application/x-ms-application',
'apr' => 'application/vnd.lotus-approach',
@ -53,6 +55,7 @@ $mimeTypes = [
'atx' => 'application/vnd.antix.game-component',
'au' => 'audio/basic',
'avi' => 'video/x-msvideo',
'avif' => 'image/avif',
'aw' => 'application/applixware',
'azf' => 'application/vnd.airzip.filesecure.azf',
'azs' => 'application/vnd.airzip.filesecure.azs',
@ -279,6 +282,7 @@ $mimeTypes = [
'geo' => 'application/vnd.dynageo',
'gex' => 'application/vnd.geometry-explorer',
'ggb' => 'application/vnd.geogebra.file',
'ggs' => 'application/vnd.geogebra.slides',
'ggt' => 'application/vnd.geogebra.tool',
'ghf' => 'application/vnd.groove-help',
'gif' => 'image/gif',
@ -318,12 +322,11 @@ $mimeTypes = [
'htke' => 'application/vnd.kenameaapp',
'htm' => 'text/html',
'html' => 'text/html',
'hvd' => 'application/vnd.yamaha.hv-dic',
'hvp' => 'application/vnd.yamaha.hv-voice',
'hvs' => 'application/vnd.yamaha.hv-script',
'i2g' => 'application/vnd.intergeo',
'icc' => 'application/vnd.iccprofile',
0 => 'application/vnd.lotus-1-2-3',
'hvd' => 'application/vnd.yamaha.hv-dic',
'ice' => 'x-conference/x-cooltalk',
'icm' => 'application/vnd.iccprofile',
'ico' => 'image/x-icon',
@ -356,6 +359,7 @@ $mimeTypes = [
'jam' => 'application/vnd.jam',
'jar' => 'application/java-archive',
'java' => 'text/x-java-source',
'jfif' => 'image/jpeg',
'jisp' => 'application/vnd.jisp',
'jlt' => 'application/vnd.hp-jlyt',
'jnlp' => 'application/x-java-jnlp-file',
@ -598,6 +602,8 @@ $mimeTypes = [
'pgn' => 'application/x-chess-pgn',
'pgp' => 'application/pgp-encrypted',
'pic' => 'image/x-pict',
'pjp' => 'image/jpeg',
'pjpeg' => 'image/jpeg',
'pkg' => 'application/octet-stream',
'pki' => 'application/pkixcmp',
'pkipath' => 'application/pkix-pkipath',
@ -882,6 +888,7 @@ $mimeTypes = [
'vxml' => 'application/voicexml+xml',
'w3d' => 'application/x-director',
'wad' => 'application/x-doom',
'wasm' => 'application/wasm',
'wav' => 'audio/x-wav',
'wax' => 'audio/x-ms-wax',
'wbmp' => 'image/vnd.wap.wbmp',
@ -996,8 +1003,9 @@ $mimeTypes = [
'zmm' => 'application/vnd.handheld-entertainment+xml',
];
if (PHP_VERSION_ID >= 80100) {
# fix for bundled libmagic bug, see also https://github.com/yiisoft/yii2/issues/19925
if ((PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80122) || (PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80209)) {
$mimeTypes = array_replace($mimeTypes, array('xz' => 'application/octet-stream'));
}
return $mimeTypes;
return $mimeTypes;

View File

@ -460,7 +460,7 @@ class Formatter extends Component
}
$method = 'as' . $format;
if ($this->hasMethod($method)) {
return call_user_func_array([$this, $method], $params);
return call_user_func_array([$this, $method], array_values($params));
}
throw new InvalidArgumentException("Unknown format type: $format");

View File

@ -106,12 +106,18 @@ class FileTarget extends Target
*/
public function export()
{
$text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n";
$trimmedText = trim($text);
if (empty($trimmedText)) {
return; // No messages to export, so we exit the function early
}
if (strpos($this->logFile, '://') === false || strncmp($this->logFile, 'file://', 7) === 0) {
$logPath = dirname($this->logFile);
FileHelper::createDirectory($logPath, $this->dirMode, true);
}
$text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n";
if (($fp = @fopen($this->logFile, 'a')) === false) {
throw new InvalidConfigException("Unable to append to log file: {$this->logFile}");
}

View File

@ -882,6 +882,9 @@ class DbManager extends BaseManager
])->execute();
unset($this->checkAccessAssignments[(string) $userId]);
$this->invalidateCache();
return $assignment;
}
@ -895,9 +898,13 @@ class DbManager extends BaseManager
}
unset($this->checkAccessAssignments[(string) $userId]);
return $this->db->createCommand()
$result = $this->db->createCommand()
->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name])
->execute() > 0;
$this->invalidateCache();
return $result;
}
/**
@ -910,9 +917,13 @@ class DbManager extends BaseManager
}
unset($this->checkAccessAssignments[(string) $userId]);
return $this->db->createCommand()
$result = $this->db->createCommand()
->delete($this->assignmentTable, ['user_id' => (string) $userId])
->execute() > 0;
$this->invalidateCache();
return $result;
}
/**

View File

@ -294,10 +294,9 @@ class Serializer extends Component
*/
protected function serializeModels(array $models)
{
list($fields, $expand) = $this->getRequestedFields();
foreach ($models as $i => $model) {
if ($model instanceof Arrayable) {
$models[$i] = $model->toArray($fields, $expand);
$models[$i] = $this->serializeModel($model);
} elseif (is_array($model)) {
$models[$i] = ArrayHelper::toArray($model);
}

View File

@ -17,10 +17,6 @@ use yii\helpers\Url;
*
* For more details and usage information on Controller, see the [guide article on controllers](guide:structure-controllers).
*
* @property Request $request The request object.
* @property Response $response The response object.
* @property View $view The view object that can be used to render views or view files.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
@ -145,6 +141,7 @@ class Controller extends \yii\base\Controller
} elseif (
PHP_VERSION_ID >= 70000
&& ($type = $param->getType()) !== null
&& method_exists($type, 'isBuiltin')
&& $type->isBuiltin()
&& ($params[$name] !== null || !$type->allowsNull())
) {

View File

@ -57,8 +57,8 @@ class Cookie extends \yii\base\BaseObject
*/
public $domain = '';
/**
* @var int the timestamp at which the cookie expires. This is the server timestamp.
* Defaults to 0, meaning "until the browser is closed".
* @var int|string|\DateTimeInterface|null the timestamp or date at which the cookie expires. This is the server timestamp.
* Defaults to 0, meaning "until the browser is closed" (the same applies to `null`).
*/
public $expire = 0;
/**

View File

@ -18,7 +18,6 @@ use yii\base\InvalidCallException;
* For more details and usage information on CookieCollection, see the [guide article on handling cookies](guide:runtime-sessions-cookies).
*
* @property-read int $count The number of cookies in the collection.
* @property-read ArrayIterator $iterator An iterator for traversing the cookies in the collection.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
@ -52,7 +51,7 @@ class CookieCollection extends BaseObject implements \IteratorAggregate, \ArrayA
* Returns an iterator for traversing the cookies in the collection.
* This method is required by the SPL interface [[\IteratorAggregate]].
* It will be implicitly called when you use `foreach` to traverse the collection.
* @return ArrayIterator an iterator for traversing the cookies in the collection.
* @return ArrayIterator<string, Cookie> an iterator for traversing the cookies in the collection.
*/
#[\ReturnTypeWillChange]
public function getIterator()
@ -114,7 +113,18 @@ class CookieCollection extends BaseObject implements \IteratorAggregate, \ArrayA
public function has($name)
{
return isset($this->_cookies[$name]) && $this->_cookies[$name]->value !== ''
&& ($this->_cookies[$name]->expire === null || $this->_cookies[$name]->expire === 0 || $this->_cookies[$name]->expire >= time());
&& ($this->_cookies[$name]->expire === null
|| $this->_cookies[$name]->expire === 0
|| (
(is_string($this->_cookies[$name]->expire) && strtotime($this->_cookies[$name]->expire) >= time())
|| (
interface_exists('\\DateTimeInterface')
&& $this->_cookies[$name]->expire instanceof \DateTimeInterface
&& $this->_cookies[$name]->expire->getTimestamp() >= time()
)
|| $this->_cookies[$name]->expire >= time()
)
);
}
/**
@ -175,7 +185,7 @@ class CookieCollection extends BaseObject implements \IteratorAggregate, \ArrayA
/**
* Returns the collection as a PHP array.
* @return array the array representation of the collection.
* @return Cookie[] the array representation of the collection.
* The array keys are cookie names, and the array values are the corresponding cookie objects.
*/
public function toArray()

View File

@ -13,9 +13,6 @@ use yii\base\BaseObject;
/**
* HeaderCollection is used by [[Response]] to maintain the currently registered HTTP headers.
*
* @property-read int $count The number of headers in the collection.
* @property-read \ArrayIterator $iterator An iterator for traversing the headers in the collection.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/

View File

@ -1220,6 +1220,8 @@ class Request extends \yii\base\Request
return null;
}
private $_ip = null;
/**
* Returns the user IP address.
* The IP is determined using headers and / or `$_SERVER` variables.
@ -1227,8 +1229,14 @@ class Request extends \yii\base\Request
*/
public function getUserIP()
{
$ip = $this->getUserIpFromIpHeaders();
return $ip === null ? $this->getRemoteIP() : $ip;
if ($this->_ip === null) {
$this->_ip = $this->getUserIpFromIpHeaders();
if ($this->_ip === null) {
$this->_ip = $this->getRemoteIP();
}
}
return $this->_ip;
}
/**
@ -1902,6 +1910,8 @@ class Request extends \yii\base\Request
return null;
}
private $_secureForwardedHeaderTrustedParts;
/**
* Gets only trusted `Forwarded` header parts
*
@ -1911,6 +1921,10 @@ class Request extends \yii\base\Request
*/
protected function getSecureForwardedHeaderTrustedParts()
{
if ($this->_secureForwardedHeaderTrustedParts !== null) {
return $this->_secureForwardedHeaderTrustedParts;
}
$validator = $this->getIpValidator();
$trustedHosts = [];
foreach ($this->trustedHosts as $trustedCidr => $trustedCidrOrHeaders) {
@ -1921,9 +1935,14 @@ class Request extends \yii\base\Request
}
$validator->setRanges($trustedHosts);
return array_filter($this->getSecureForwardedHeaderParts(), function ($headerPart) use ($validator) {
return isset($headerPart['for']) ? !$validator->validate($headerPart['for']) : true;
});
$this->_secureForwardedHeaderTrustedParts = array_filter(
$this->getSecureForwardedHeaderParts(),
function ($headerPart) use ($validator) {
return isset($headerPart['for']) ? !$validator->validate($headerPart['for']) : true;
}
);
return $this->_secureForwardedHeaderTrustedParts;
}
private $_secureForwardedHeaderParts;

View File

@ -10,6 +10,7 @@ namespace yii\web;
use Yii;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\base\InvalidRouteException;
use yii\helpers\FileHelper;
use yii\helpers\Inflector;
use yii\helpers\StringHelper;
@ -400,12 +401,21 @@ class Response extends \yii\base\Response
}
foreach ($this->getCookies() as $cookie) {
$value = $cookie->value;
if ($cookie->expire != 1 && isset($validationKey)) {
$expire = $cookie->expire;
if (is_string($expire)) {
$expire = strtotime($expire);
} elseif (interface_exists('\\DateTimeInterface') && $expire instanceof \DateTimeInterface) {
$expire = $expire->getTimestamp();
}
if ($expire === null || $expire === false) {
$expire = 0;
}
if ($expire != 1 && isset($validationKey)) {
$value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
}
if (PHP_VERSION_ID >= 70300) {
setcookie($cookie->name, $value, [
'expires' => $cookie->expire,
'expires' => $expire,
'path' => $cookie->path,
'domain' => $cookie->domain,
'secure' => $cookie->secure,
@ -419,7 +429,7 @@ class Response extends \yii\base\Response
if (!is_null($cookie->sameSite)) {
$cookiePath .= '; samesite=' . $cookie->sameSite;
}
setcookie($cookie->name, $value, $cookie->expire, $cookiePath, $cookie->domain, $cookie->secure, $cookie->httpOnly);
setcookie($cookie->name, $value, $expire, $cookiePath, $cookie->domain, $cookie->secure, $cookie->httpOnly);
}
}
}
@ -886,12 +896,13 @@ class Response extends \yii\base\Response
}
$request = Yii::$app->getRequest();
$normalizedUrl = Url::to($url);
if (
$normalizedUrl !== null
&& strncmp($normalizedUrl, '/', 1) === 0
&& strncmp($normalizedUrl, '//', 2) !== 0
) {
$normalizedUrl = $request->getHostInfo() . $normalizedUrl;
if ($normalizedUrl !== null) {
if (preg_match('/\n/', $normalizedUrl)) {
throw new InvalidRouteException('Route with new line character detected "' . $normalizedUrl . '".');
}
if (strncmp($normalizedUrl, '/', 1) === 0 && strncmp($normalizedUrl, '//', 2) !== 0) {
$normalizedUrl = $request->getHostInfo() . $normalizedUrl;
}
}
if ($checkAjax && $request->getIsAjax()) {

View File

@ -57,7 +57,6 @@ use yii\base\InvalidConfigException;
* @property bool $hasSessionId Whether the current request has sent the session ID.
* @property string $id The current session ID.
* @property-read bool $isActive Whether the session has started.
* @property-read SessionIterator $iterator An iterator for traversing the session variables.
* @property string $name The current session name.
* @property string $savePath The current session save path, defaults to '/tmp'.
* @property int $timeout The number of seconds after which data will be seen as 'garbage' and cleaned up. The
@ -406,7 +405,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
* 'sameSite' => PHP_VERSION_ID >= 70300 ? yii\web\Cookie::SAME_SITE_LAX : null,
* ]
* ```
* See https://www.owasp.org/index.php/SameSite for more information about `sameSite`.
* See https://owasp.org/www-community/SameSite for more information about `sameSite`.
*
* @throws InvalidArgumentException if the parameters are incomplete.
* @see https://www.php.net/manual/en/function.session-set-cookie-params.php

View File

@ -133,6 +133,7 @@ class View extends \yii\base\View
private $_assetManager;
/**
* Whether [[endPage()]] has been called and all files have been registered
* @var bool
@ -140,7 +141,6 @@ class View extends \yii\base\View
*/
protected $isPageEnded = false;
/**
* Marks the position of an HTML head section.
*/

View File

@ -21,7 +21,6 @@
<file>framework/web/ResponseFormatterInterface.php</file>
<file>framework/.phpstorm.meta.php</file>
<directory suffix="Exception.php">framework/base</directory>
<directory suffix=".php">framework/db/mssql</directory>
<directory suffix=".php">framework/bootstrap</directory>
</blacklist>
</filter>

View File

@ -123,6 +123,39 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
$this->assertEquals($expected, $actual, $message);
}
/**
* Asserting two strings equality ignoring unicode whitespaces.
* @param string $expected
* @param string $actual
* @param string $message
*/
protected function assertEqualsAnyWhitespace($expected, $actual, $message = ''){
$expected = $this->sanitizeWhitespaces($expected);
$actual = $this->sanitizeWhitespaces($actual);
$this->assertEquals($expected, $actual, $message);
}
/**
* Asserts that two variables have the same type and value and sanitizes value if it is a string.
* Used on objects, it asserts that two variables reference
* the same object.
*
* @param mixed $expected
* @param mixed $actual
* @param string $message
*/
protected function assertSameAnyWhitespace($expected, $actual, $message = ''){
if (is_string($expected)) {
$expected = $this->sanitizeWhitespaces($expected);
}
if (is_string($actual)) {
$actual = $this->sanitizeWhitespaces($actual);
}
$this->assertSame($expected, $actual, $message);
}
/**
* Asserts that a haystack contains a needle ignoring line endings.
*
@ -138,6 +171,17 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
$this->assertContains($needle, $haystack, $message);
}
/**
* Replaces unicode whitespaces with standard whitespace
*
* @see https://github.com/yiisoft/yii2/issues/19868 (ICU 72 changes)
* @param $string
* @return string
*/
protected function sanitizeWhitespaces($string){
return preg_replace("/[\pZ\pC]/u", " ", $string);
}
/**
* Invokes a inaccessible method.
* @param $object

View File

@ -0,0 +1,27 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yiiunit\data\ar;
/**
* @property int $attr1
* @property int $attr2
*/
class NoAutoLabels extends ActiveRecord
{
public function attributeLabels()
{
return [
'attr1' => 'Label for attr1',
];
}
public function generateAttributeLabel($name)
{
throw new \yii\base\InvalidArgumentException('Label not defined!');
}
}

View File

@ -37,6 +37,20 @@ class DynamicModelTest extends TestCase
$this->assertTrue($model->hasErrors('age'));
}
public function testValidateDataWithPostData()
{
$post = [
'name' => 'long name',
];
$model = DynamicModel::validateData($post, [
[['email', 'name'], 'required'],
['age', 'default', 'value' => 18],
]);
$this->assertTrue($model->hasErrors());
$this->assertTrue($model->hasErrors('email'));
$this->assertEquals(18, $model->age);
}
public function testAddRule()
{
$model = new DynamicModel();

View File

@ -23,7 +23,7 @@ class ApcCacheTest extends CacheTestCase
*/
protected function getCacheInstance()
{
if (!extension_loaded('apc')) {
if (!extension_loaded('apc') && !extension_loaded('apcu')) {
$this->markTestSkipped('APC not installed. Skipping.');
} elseif ('cli' === PHP_SAPI && !ini_get('apc.enable_cli')) {
$this->markTestSkipped('APC cli is not enabled. Skipping.');
@ -33,7 +33,9 @@ class ApcCacheTest extends CacheTestCase
$this->markTestSkipped('APC is installed but not enabled. Skipping.');
}
if ($this->_cacheInstance === null) {
if ($this->_cacheInstance === null && PHP_VERSION_ID >= 70400) {
$this->_cacheInstance = new ApcCache(['useApcu' => true]);
} elseif ($this->_cacheInstance === null) {
$this->_cacheInstance = new ApcCache();
}

View File

@ -27,7 +27,9 @@ class MemCachedTest extends CacheTestCase
$this->markTestSkipped('memcached not installed. Skipping.');
}
if (PHP_VERSION_ID >= 80100 && version_compare(phpversion('memcached'), '3.1.5', '<=')) {
if (
PHP_VERSION_ID >= 80100 && version_compare(phpversion('memcached'), '3.1.5', '<=')
) {
$php_version = phpversion();
$memcached_version = phpversion('memcached');
$this->markTestSkipped("memcached version $memcached_version is not ready for PHP $php_version. Skipping.");

View File

@ -58,6 +58,121 @@ class TableTest extends TestCase
║ testcontent21 │ testcontent22 │ testcontent23 ║
╚═══════════════╧═══════════════╧═══════════════╝
EXPECTED;
$tableContent = $table
->setHeaders($headers)
->setRows($rows)
->setScreenWidth(200)
->run();
$this->assertEqualsWithoutLE($expected, $tableContent);
}
public function getMultiLineTableData()
{
return [
[
['test1', 'test2', 'test3' . PHP_EOL . 'multiline'],
[
['test' . PHP_EOL . 'content1', 'testcontent2', 'test' . PHP_EOL . 'content3'],
[
'testcontent21',
'testcontent22' . PHP_EOL
. 'loooooooooooooooooooooooooooooooooooong' . PHP_EOL
. 'content',
'testcontent23' . PHP_EOL
. 'loooooooooooooooooooooooooooooooooooong content'
],
]
],
[
['key1' => 'test1', 'key2' => 'test2', 'key3' => 'test3' . PHP_EOL . 'multiline'],
[
[
'key1' => 'test' . PHP_EOL . 'content1',
'key2' => 'testcontent2',
'key3' => 'test' . PHP_EOL . 'content3'
],
[
'key1' => 'testcontent21',
'key2' => 'testcontent22' . PHP_EOL
. 'loooooooooooooooooooooooooooooooooooong' . PHP_EOL
. 'content',
'key3' => 'testcontent23' . PHP_EOL
. 'loooooooooooooooooooooooooooooooooooong content'
],
]
]
];
}
/**
* @dataProvider getMultiLineTableData
*/
public function testMultiLineTable($headers, $rows)
{
$table = new Table();
$expected = <<<'EXPECTED'
╔═════════════╤═════════════════════════════════════╤═════════════════════════════════════════════╗
║ test1 │ test2 │ test3 ║
║ │ │ multiline ║
╟─────────────┼─────────────────────────────────────┼─────────────────────────────────────────────╢
║ test │ testcontent2 │ test ║
║ content1 │ │ content3 ║
╟─────────────┼─────────────────────────────────────┼─────────────────────────────────────────────╢
║ testcontent │ testcontent22 │ testcontent23 ║
║ 21 │ loooooooooooooooooooooooooooooooooo │ loooooooooooooooooooooooooooooooooooong con ║
║ │ oong │ tent ║
║ │ content │ ║
╚═════════════╧═════════════════════════════════════╧═════════════════════════════════════════════╝
EXPECTED;
$tableContent = $table
->setHeaders($headers)
->setRows($rows)
->setScreenWidth(100)
->run();
$this->assertEqualsWithoutLE($expected, $tableContent);
}
public function getNumericTableData()
{
return [
[
[1, 2, 3],
[
[1, 1.2, -1.3],
[-2, 2.2, 2.3],
]
],
[
['key1' => 1, 'key2' => 2, 'key3' => 3],
[
['key1' => 1, 'key2' => 1.2, 'key3' => -1.3],
['key1' => -2, 'key2' => 2.2, 'key3' => 2.3],
]
]
];
}
/**
* @dataProvider getNumericTableData
*/
public function testNumericTable($headers, $rows)
{
$table = new Table();
$expected = <<<'EXPECTED'
╔════╤═════╤══════╗
║ 1 │ 2 │ 3 ║
╟────┼─────┼──────╢
║ 1 │ 1.2 │ -1.3 ║
╟────┼─────┼──────╢
║ -2 │ 2.2 │ 2.3 ║
╚════╧═════╧══════╝
EXPECTED;
$tableContent = $table
@ -101,7 +216,8 @@ EXPECTED;
╔═══════════════╤═══════════════╤══════════════╗
║ test1 │ test2 │ test3 ║
╟───────────────┼───────────────┼──────────────╢
testcontent1 │ testcontent2 │ testcontent3 ║
• col1 │ testcontent2 │ testcontent3 ║
║ • col2 │ │ ║
╟───────────────┼───────────────┼──────────────╢
║ testcontent21 │ testcontent22 │ • col1 ║
║ │ │ • col2 ║
@ -111,7 +227,7 @@ EXPECTED;
$this->assertEqualsWithoutLE($expected, $table->setHeaders(['test1', 'test2', 'test3'])
->setRows([
['testcontent1', 'testcontent2', 'testcontent3'],
[['key1' => 'col1', 'key2' => 'col2'], 'testcontent2', 'testcontent3'],
['testcontent21', 'testcontent22', ['col1', 'col2']],
])->setScreenWidth(200)->run()
);
@ -141,6 +257,35 @@ EXPECTED;
);
}
public function testLongerListPrefix()
{
$table = new Table();
$expected = <<<'EXPECTED'
╔═════════════════════════════════╤═════════════════════════════════╤═════════════════════════════╗
║ test1 │ test2 │ test3 ║
╟─────────────────────────────────┼─────────────────────────────────┼─────────────────────────────╢
║ testcontent1 │ testcontent2 │ testcontent3 ║
╟─────────────────────────────────┼─────────────────────────────────┼─────────────────────────────╢
║ testcontent21 with looooooooooo │ testcontent22 with looooooooooo │ -- col1 with looooooooooooo ║
║ ooooooooooooong content │ ooooooooooooong content │ ooooooooooong content ║
║ │ │ -- col2 with long content ║
╚═════════════════════════════════╧═════════════════════════════════╧═════════════════════════════╝
EXPECTED;
$this->assertEqualsWithoutLE($expected, $table->setHeaders(['test1', 'test2', 'test3'])
->setRows([
['testcontent1', 'testcontent2', 'testcontent3'],
[
'testcontent21 with loooooooooooooooooooooooong content',
'testcontent22 with loooooooooooooooooooooooong content',
['col1 with loooooooooooooooooooooooong content', 'col2 with long content']
],
])->setScreenWidth(100)->setListPrefix('-- ')->run()
);
}
public function testCustomChars()
{
$table = new Table();

View File

@ -2192,4 +2192,87 @@ abstract class ActiveRecordTest extends DatabaseTestCase
$this->assertNotNull($order->virtualCustomer);
}
public function labelTestModelProvider()
{
$data = [];
// Model 2 and 3 are represented by objects.
$model1 = new LabelTestModel1();
$model2 = new LabelTestModel2();
$model3 = new LabelTestModel3();
$model2->populateRelation('model3', $model3);
$model1->populateRelation('model2', $model2);
$data[] = [$model1];
// Model 2 and 3 are represented by arrays instead of objects.
$model1 = new LabelTestModel1();
$model2 = ['model3' => []];
$model1->populateRelation('model2', $model2);
$data[] = [$model1];
return $data;
}
/**
* @dataProvider labelTestModelProvider
* @param \yii\db\ActiveRecord $model
*/
public function testGetAttributeLabel($model)
{
$this->assertEquals('model3.attr1 from model2', $model->getAttributeLabel('model2.model3.attr1'));
$this->assertEquals('attr2 from model3', $model->getAttributeLabel('model2.model3.attr2'));
$this->assertEquals('model3.attr3 from model2', $model->getAttributeLabel('model2.model3.attr3'));
$attr = 'model2.doesNotExist.attr1';
$this->assertEquals($model->generateAttributeLabel($attr), $model->getAttributeLabel($attr));
}
}
class LabelTestModel1 extends \yii\db\ActiveRecord
{
public function attributes()
{
return [];
}
public function getModel2()
{
return $this->hasOne(LabelTestModel2::className(), []);
}
}
class LabelTestModel2 extends \yii\db\ActiveRecord
{
public function attributes()
{
return [];
}
public function getModel3()
{
return $this->hasOne(LabelTestModel3::className(), []);
}
public function attributeLabels()
{
return [
'model3.attr1' => 'model3.attr1 from model2', // Override label defined in model3.
'model3.attr3' => 'model3.attr3 from model2', // Define label not defined in model3.
];
}
}
class LabelTestModel3 extends \yii\db\ActiveRecord
{
public function attributes()
{
return ['attr1', 'attr2', 'attr3'];
}
public function attributeLabels()
{
return [
'attr1' => 'attr1 from model3',
'attr2' => 'attr2 from model3',
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace yiiunit\framework\db;
use yiiunit\data\ar\ActiveRecord;
abstract class BaseActiveRecordTest extends DatabaseTestCase
{
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
}
public function provideArrayValueWithChange()
{
return [
'not an associative array with data change' => [
[1, 2, 3],
[1, 3, 2],
],
'associative array with data change case 1' => [
['pineapple' => 2, 'apple' => 5, 'banana' => 1],
['apple' => 5, 'pineapple' => 1, 'banana' => 3],
],
'associative array with data change case 2' => [
['pineapple' => 2, 'apple' => 5, 'banana' => 1],
['pineapple' => 2, 'apple' => 3, 'banana' => 1],
],
'filling an empty array' => [
[],
['pineapple' => 3, 'apple' => 1, 'banana' => 1],
],
'zeroing the array' => [
['pineapple' => 3, 'apple' => 1, 'banana' => 17],
[],
],
];
}
}

View File

@ -1525,4 +1525,23 @@ SQL;
$db->createCommand()->setSql("SELECT :p1")->bindValues([':p1' => [2, \PDO::PARAM_STR]]);
$this->assertTrue(true);
}
public function testBindValuesSupportsEnums()
{
if (version_compare(PHP_VERSION, '8.1.0') >= 0) {
$db = $this->getConnection();
$command = $db->createCommand();
$command->setSql('SELECT :p1')->bindValues([':p1' => enums\Status::ACTIVE]);
$this->assertSame('ACTIVE', $command->params[':p1']);
$command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeString::ACTIVE]);
$this->assertSame('active', $command->params[':p1']);
$command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeInt::ACTIVE]);
$this->assertSame(1, $command->params[':p1']);
} else {
$this->markTestSkipped('Enums are not supported in PHP < 8.1');
}
}
}

View File

@ -469,7 +469,7 @@ abstract class ConnectionTest extends DatabaseTestCase
/**
* Test whether slave connection is recovered when call getSlavePdo() after close().
* Test whether slave connection is recovered when call getSlavePdo(true) after close().
*
* @see https://github.com/yiisoft/yii2/issues/14165
*/

View File

@ -757,7 +757,7 @@ abstract class SchemaTest extends DatabaseTestCase
}
$connection = $this->getConnection(false);
$connection->getSlavePdo()->setAttribute(PDO::ATTR_CASE, PDO::CASE_UPPER);
$connection->getSlavePdo(true)->setAttribute(PDO::ATTR_CASE, PDO::CASE_UPPER);
$constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true);
$this->assertMetadataEquals($expected, $constraints);
}
@ -775,7 +775,7 @@ abstract class SchemaTest extends DatabaseTestCase
}
$connection = $this->getConnection(false);
$connection->getSlavePdo()->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$connection->getSlavePdo(true)->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true);
$this->assertMetadataEquals($expected, $constraints);
}

View File

@ -57,7 +57,7 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
public function testCommentColumn()
{
$version = $this->getQueryBuilder(false)->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
$version = $this->getQueryBuilder(false)->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_SERVER_VERSION);
if (version_compare($version, '10.0', '<')) {
$this->markTestSkipped('Comments on columns are supported starting with CUBRID 10.0.');
return;

View File

@ -0,0 +1,9 @@
<?php
namespace yiiunit\framework\db\enums;
enum Status
{
case ACTIVE;
case INACTIVE;
}

View File

@ -0,0 +1,9 @@
<?php
namespace yiiunit\framework\db\enums;
enum StatusTypeInt: int
{
case ACTIVE = 1;
case INACTIVE = 0;
}

View File

@ -0,0 +1,9 @@
<?php
namespace yiiunit\framework\db\enums;
enum StatusTypeString: string
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';
}

View File

@ -134,22 +134,19 @@ class CommandTest extends \yiiunit\framework\db\CommandTest
{
$db = $this->getConnection();
$testData = json_encode(['test' => 'string', 'test2' => 'integer']);
$qb = $db->getQueryBuilder();
$testData = json_encode(['test' => 'string', 'test2' => 'integer'], JSON_THROW_ON_ERROR);
$params = [];
$qb = $db->getQueryBuilder();
$sql = $qb->upsert('T_upsert_varbinary', ['id' => 1, 'blob_col' => $testData] , ['blob_col' => $testData], $params);
$sql = $qb->upsert('T_upsert_varbinary', ['id' => 1, 'blob_col' => $testData], ['blob_col' => $testData], $params);
$result = $db->createCommand($sql, $params)->execute();
$this->assertEquals(1, $result);
$query = (new Query())
->select(['convert(nvarchar(max),blob_col) as blob_col'])
->from('T_upsert_varbinary')
->where(['id' => 1]);
$this->assertSame(1, $result);
$query = (new Query())->select(['blob_col'])->from('T_upsert_varbinary')->where(['id' => 1]);
$resultData = $query->createCommand($db)->queryOne();
$this->assertEquals($testData, $resultData['blob_col']);
$this->assertSame($testData, $resultData['blob_col']);
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yiiunit\framework\db\mssql;
use yii\caching\FileCache;
use yii\db\Query;
use yiiunit\framework\db\DatabaseTestCase;
/**
* @group db
* @group mssql
*/
class QueryCacheTest extends DatabaseTestCase
{
protected $driverName = 'sqlsrv';
public function testQueryCacheFileCache()
{
$db = $this->getConnection();
$db->enableQueryCache = true;
$db->queryCache = new FileCache(['cachePath' => '@yiiunit/runtime/cache']);
$db->createCommand()->delete('type')->execute();
$db->createCommand()->insert('type', [
'int_col' => $key = 1,
'char_col' => '',
'char_col2' => '6a3ce1a0bffe8eeb6fa986caf443e24c',
'float_col' => 0.0,
'blob_col' => 'a:1:{s:13:"template";s:1:"1";}',
'bool_col' => true,
])->execute();
$function = function($db) use ($key){
return (new Query())
->select(['blob_col'])
->from('type')
->where(['int_col' => $key])
->createCommand($db)
->queryScalar();
};
// First run return
$result = $db->cache($function);
$this->assertSame('a:1:{s:13:"template";s:1:"1";}', $result);
// After the request has been cached return
$result = $db->cache($function);
$this->assertSame('a:1:{s:13:"template";s:1:"1";}', $result);
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yiiunit\framework\db\mssql\Type;
use yii\db\Query;
use yiiunit\framework\db\DatabaseTestCase;
/**
* @group db
* @group mssql
*/
class VarbinaryTest extends DatabaseTestCase
{
protected $driverName = 'sqlsrv';
public function testVarbinary()
{
$db = $this->getConnection();
$db->createCommand()->delete('type')->execute();
$db->createCommand()->insert('type', [
'int_col' => $key = 1,
'char_col' => '',
'char_col2' => '6a3ce1a0bffe8eeb6fa986caf443e24c',
'float_col' => 0.0,
'blob_col' => 'a:1:{s:13:"template";s:1:"1";}',
'bool_col' => true,
])->execute();
$result = (new Query())
->select(['blob_col'])
->from('type')
->where(['int_col' => $key])
->createCommand($db)
->queryScalar();
$this->assertSame('a:1:{s:13:"template";s:1:"1";}', $result);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace yiiunit\framework\db\mysql;
use yiiunit\data\ar\Storage;
class BaseActiveRecordTest extends \yiiunit\framework\db\BaseActiveRecordTest
{
public $driverName = 'mysql';
/**
* @see https://github.com/yiisoft/yii2/issues/19872
*
* @dataProvider provideArrayValueWithChange
*/
public function testJsonDirtyAttributesWithDataChange($actual, $modified)
{
if (version_compare($this->getConnection()->getSchema()->getServerVersion(), '5.7', '<')) {
$this->markTestSkipped('JSON columns are not supported in MySQL < 5.7');
}
if (version_compare(PHP_VERSION, '5.6', '<')) {
$this->markTestSkipped('JSON columns are not supported in PDO for PHP < 5.6');
}
$createdStorage = new Storage(['data' => $actual]);
$createdStorage->save();
$foundStorage = Storage::find()->limit(1)->one();
$this->assertNotNull($foundStorage);
$foundStorage->data = $modified;
$this->assertSame(['data' => $modified], $foundStorage->getDirtyAttributes());
}
}

View File

@ -125,7 +125,7 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
/**
* @link https://github.com/yiisoft/yii2/issues/14367
*/
$mysqlVersion = $this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
$mysqlVersion = $this->getDb()->getSlavePdo(true)->getAttribute(\PDO::ATTR_SERVER_VERSION);
$supportsFractionalSeconds = version_compare($mysqlVersion,'5.6.4', '>=');
if ($supportsFractionalSeconds) {
$expectedValues = [

View File

@ -119,7 +119,12 @@ class DeadLockTest extends \yiiunit\framework\db\mysql\ConnectionTest
. ($logContent ? ". Shared children log:\n$logContent" : '')
);
}
$this->assertEquals(1, $deadlockHitCount, "exactly one child must hit deadlock; shared children log:\n" . $logContent);
if (version_compare($this->getConnection()->getSchema()->getServerVersion(), '8.0', '<')) {
$this->assertEquals(1, $deadlockHitCount, "exactly one child must hit deadlock; shared children log:\n" . $logContent);
} else {
$this->assertEquals(0, $deadlockHitCount, "exactly zero children must hit deadlock; shared children log:\n" . $logContent);
}
}
/**

View File

@ -0,0 +1,46 @@
<?php
namespace yiiunit\framework\db\pgsql;
use yii\db\JsonExpression;
use yiiunit\data\ar\ActiveRecord;
class BaseActiveRecordTest extends \yiiunit\framework\db\BaseActiveRecordTest
{
public $driverName = 'pgsql';
/**
* @see https://github.com/yiisoft/yii2/issues/19872
*
* @dataProvider provideArrayValueWithChange
*/
public function testJsonDirtyAttributesWithDataChange($actual, $modified)
{
$createdStorage = new ArrayAndJsonType([
'json_col' => new JsonExpression($actual),
]);
$createdStorage->save();
$foundStorage = ArrayAndJsonType::find()->limit(1)->one();
$this->assertNotNull($foundStorage);
$foundStorage->json_col = $modified;
$this->assertSame(['json_col' => $modified], $foundStorage->getDirtyAttributes());
}
}
/**
* {@inheritdoc}
* @property array id
* @property array json_col
*/
class ArrayAndJsonType extends ActiveRecord
{
public static function tableName()
{
return '{{%array_and_json_types}}';
}
}

View File

@ -7,6 +7,8 @@
namespace yiiunit\framework\db\sqlite;
use yii\db\sqlite\Schema;
/**
* @group db
* @group sqlite
@ -109,4 +111,60 @@ SQL;
return $parent;
}
public function testResetSequence()
{
$db = $this->getConnection();
if ($db->getTableSchema('reset_sequence', true) !== null) {
$db->createCommand()->dropTable('reset_sequence')->execute();
}
// create table reset_sequence
$db->createCommand()->createTable(
'reset_sequence',
[
'id' => Schema::TYPE_PK,
'description' => Schema::TYPE_TEXT,
]
)->execute();
// ensure auto increment is working
$db->createCommand()->insert('reset_sequence', ['description' => 'test'])->execute();
$this->assertEquals(1, $db->createCommand('SELECT MAX([[id]]) FROM {{reset_sequence}}')->queryScalar());
// remove all records
$db->createCommand()->delete('reset_sequence')->execute();
$this->assertEquals(0, $db->createCommand('SELECT COUNT(*) FROM {{reset_sequence}}')->queryScalar());
// counter should be reset to 1
$db->createCommand()->resetSequence('reset_sequence')->execute();
$db->createCommand()->insert('reset_sequence', ['description' => 'test'])->execute();
$this->assertEquals(1, $db->createCommand('SELECT COUNT(*) FROM {{reset_sequence}}')->queryScalar());
$this->assertEquals(1, $db->createCommand('SELECT MAX([[id]]) FROM {{reset_sequence}}')->queryScalar());
// counter should be reset to 5, so next record gets ID 5
$db->createCommand()->resetSequence('reset_sequence', 5)->execute();
$db->createCommand()->insert('reset_sequence', ['description' => 'test'])->execute();
$this->assertEquals(2, $db->createCommand('SELECT COUNT(*) FROM {{reset_sequence}}')->queryScalar());
$this->assertEquals(5, $db->createCommand('SELECT MAX([[id]]) FROM {{reset_sequence}}')->queryScalar());
}
public function testResetSequenceExceptionTableNoExist()
{
$this->expectException('yii\base\InvalidArgumentException');
$this->expectExceptionMessage('Table not found: no_exist_table');
$db = $this->getConnection();
$db->createCommand()->resetSequence('no_exist_table', 5)->execute();
}
public function testResetSequenceExceptionSquenceNoExist()
{
$this->expectException('yii\base\InvalidArgumentException');
$this->expectExceptionMessage("There is not sequence associated with table 'type'.");
$db = $this->getConnection();
$db->createCommand()->resetSequence('type', 5)->execute();
}
}

View File

@ -422,17 +422,18 @@ class AccessRuleTest extends \yiiunit\TestCase
{
$action = $this->mockAction();
$user = false;
$request = $this->mockRequest();
$rule = new AccessRule();
// by default match all IPs
$request = $this->mockRequest();
$rule->allow = true;
$this->assertTrue($rule->allows($action, $user, $request));
$rule->allow = false;
$this->assertFalse($rule->allows($action, $user, $request));
// empty IPs = match all IPs
$request = $this->mockRequest();
$rule->ips = [];
$rule->allow = true;
$this->assertTrue($rule->allows($action, $user, $request));
@ -441,6 +442,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// match, one IP
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$request = $this->mockRequest();
$rule->ips = ['127.0.0.1'];
$rule->allow = true;
$this->assertTrue($rule->allows($action, $user, $request));
@ -449,6 +451,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// no match, one IP
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$request = $this->mockRequest();
$rule->ips = ['192.168.0.1'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
@ -457,12 +460,14 @@ class AccessRuleTest extends \yiiunit\TestCase
// no partial match, one IP
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$request = $this->mockRequest();
$rule->ips = ['127.0.0.10'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
$rule->allow = false;
$this->assertNull($rule->allows($action, $user, $request));
$_SERVER['REMOTE_ADDR'] = '127.0.0.10';
$request = $this->mockRequest();
$rule->ips = ['127.0.0.1'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
@ -471,6 +476,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// match, one IP IPv6
$_SERVER['REMOTE_ADDR'] = '::1';
$request = $this->mockRequest();
$rule->ips = ['::1'];
$rule->allow = true;
$this->assertTrue($rule->allows($action, $user, $request));
@ -479,6 +485,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// no match, one IP IPv6
$_SERVER['REMOTE_ADDR'] = '::1';
$request = $this->mockRequest();
$rule->ips = ['dead::beaf::1'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
@ -487,12 +494,14 @@ class AccessRuleTest extends \yiiunit\TestCase
// no partial match, one IP IPv6
$_SERVER['REMOTE_ADDR'] = '::1';
$request = $this->mockRequest();
$rule->ips = ['::123'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
$rule->allow = false;
$this->assertNull($rule->allows($action, $user, $request));
$_SERVER['REMOTE_ADDR'] = '::123';
$request = $this->mockRequest();
$rule->ips = ['::1'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
@ -501,6 +510,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// undefined IP
$_SERVER['REMOTE_ADDR'] = null;
$request = $this->mockRequest();
$rule->ips = ['192.168.*'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
@ -512,12 +522,12 @@ class AccessRuleTest extends \yiiunit\TestCase
{
$action = $this->mockAction();
$user = false;
$request = $this->mockRequest();
$rule = new AccessRule();
// no match
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$request = $this->mockRequest();
$rule->ips = ['192.168.*'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
@ -526,6 +536,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// match
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$request = $this->mockRequest();
$rule->ips = ['127.0.*'];
$rule->allow = true;
$this->assertTrue($rule->allows($action, $user, $request));
@ -534,6 +545,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// match, IPv6
$_SERVER['REMOTE_ADDR'] = '2a01:4f8:120:7202::2';
$request = $this->mockRequest();
$rule->ips = ['2a01:4f8:120:*'];
$rule->allow = true;
$this->assertTrue($rule->allows($action, $user, $request));
@ -542,6 +554,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// no match, IPv6
$_SERVER['REMOTE_ADDR'] = '::1';
$request = $this->mockRequest();
$rule->ips = ['2a01:4f8:120:*'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
@ -553,12 +566,12 @@ class AccessRuleTest extends \yiiunit\TestCase
{
$action = $this->mockAction();
$user = false;
$request = $this->mockRequest();
$rule = new AccessRule();
// no match
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$request = $this->mockRequest();
$rule->ips = ['127.0.0.32/27'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));
@ -567,6 +580,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// match
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$request = $this->mockRequest();
$rule->ips = ['127.0.0.1/27'];
$rule->allow = true;
$this->assertTrue($rule->allows($action, $user, $request));
@ -575,6 +589,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// match, IPv6
$_SERVER['REMOTE_ADDR'] = '2a01:4f8:120:7202::2';
$request = $this->mockRequest();
$rule->ips = ['2a01:4f8:120:7202::2/127'];
$rule->allow = true;
$this->assertTrue($rule->allows($action, $user, $request));
@ -583,6 +598,7 @@ class AccessRuleTest extends \yiiunit\TestCase
// no match, IPv6
$_SERVER['REMOTE_ADDR'] = '2a01:4f8:120:7202::ffff';
$request = $this->mockRequest();
$rule->ips = ['2a01:4f8:120:7202::2/123'];
$rule->allow = true;
$this->assertNull($rule->allows($action, $user, $request));

View File

@ -7,10 +7,12 @@
namespace yiiunit\framework\grid;
use Yii;
use yii\data\ArrayDataProvider;
use yii\grid\DataColumn;
use yii\grid\GridView;
use yii\web\View;
use yiiunit\data\ar\NoAutoLabels;
/**
* @author Evgeniy Tkachenko <et.coder@gmail.com>
@ -150,4 +152,53 @@ class GridViewTest extends \yiiunit\TestCase
$this->assertTrue(preg_match("/<\/tbody><tfoot>/", $html) === 1);
}
public function testHeaderLabels()
{
// Ensure GridView does not call Model::generateAttributeLabel() to generate labels unless the labels are explicitly used.
$this->mockApplication([
'components' => [
'db' => [
'class' => \yii\db\Connection::className(),
'dsn' => 'sqlite::memory:',
],
],
]);
NoAutoLabels::$db = Yii::$app->getDb();
Yii::$app->getDb()->createCommand()->createTable(NoAutoLabels::tableName(), ['attr1' => 'int', 'attr2' => 'int'])->execute();
$urlManager = new \yii\web\UrlManager([
'baseUrl' => '/',
'scriptUrl' => '/index.php',
]);
$grid = new GridView([
'dataProvider' => new \yii\data\ActiveDataProvider([
'query' => NoAutoLabels::find(),
]),
'columns' => [
'attr1',
'attr2:text:Label for attr2',
],
]);
// NoAutoLabels::generateAttributeLabel() should not be called.
$grid->dataProvider->setSort([
'route' => '/',
'urlManager' => $urlManager,
]);
$grid->renderTableHeader();
// NoAutoLabels::generateAttributeLabel() should not be called.
$grid->dataProvider->setSort([
'route' => '/',
'urlManager' => $urlManager,
'attributes' => ['attr1', 'attr2'],
]);
$grid->renderTableHeader();
// If NoAutoLabels::generateAttributeLabel() has not been called no exception will be thrown meaning this test passed successfully.
}
}

View File

@ -135,6 +135,29 @@ class ArrayHelperTest extends TestCase
$this->assertEquals('defaultValue', $default);
}
/**
* @return void
*/
public function testRemoveWithFloat()
{
if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
$this->markTestSkipped('Using floats as array key is deprecated.');
}
$array = ['name' => 'b', 'age' => 3, 1.1 => null];
$name = ArrayHelper::remove($array, 'name');
$this->assertEquals($name, 'b');
$this->assertEquals($array, ['age' => 3, 1.1 => null]);
$floatVal = ArrayHelper::remove($array, 1.1);
$this->assertNull($floatVal);
$this->assertEquals($array, ['age' => 3]);
$default = ArrayHelper::remove($array, 'nonExisting', 'defaultValue');
$this->assertEquals('defaultValue', $default);
}
public function testRemoveValueMultiple()
{
$array = [
@ -506,14 +529,21 @@ class ArrayHelperTest extends TestCase
/**
* @see https://github.com/yiisoft/yii2/pull/11549
*/
public function test()
public function testGetValueWithFloatKeys()
{
if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
$this->markTestSkipped('Using floats as array key is deprecated.');
}
$array = [];
$array[1.0] = 'some value';
$result = ArrayHelper::getValue($array, 1.0);
$array[1.1] = 'some value';
$array[2.1] = null;
$result = ArrayHelper::getValue($array, 1.2);
$this->assertEquals('some value', $result);
$result = ArrayHelper::getValue($array, 2.2);
$this->assertNull($result);
}
public function testIndex()
@ -712,6 +742,7 @@ class ArrayHelperTest extends TestCase
'a' => 1,
'B' => 2,
];
$this->assertTrue(ArrayHelper::keyExists('a', $array));
$this->assertFalse(ArrayHelper::keyExists('b', $array));
$this->assertTrue(ArrayHelper::keyExists('B', $array));
@ -723,6 +754,27 @@ class ArrayHelperTest extends TestCase
$this->assertFalse(ArrayHelper::keyExists('c', $array, false));
}
public function testKeyExistsWithFloat()
{
if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
$this->markTestSkipped('Using floats as array key is deprecated.');
}
$array = [
1 => 3,
2.2 => 4, // Note: Floats are cast to ints, which means that the fractional part will be truncated.
3.3 => null,
];
$this->assertTrue(ArrayHelper::keyExists(1, $array));
$this->assertTrue(ArrayHelper::keyExists(1.1, $array));
$this->assertTrue(ArrayHelper::keyExists(2, $array));
$this->assertTrue(ArrayHelper::keyExists('2', $array));
$this->assertTrue(ArrayHelper::keyExists(2.2, $array));
$this->assertTrue(ArrayHelper::keyExists(3, $array));
$this->assertTrue(ArrayHelper::keyExists(3.3, $array));
}
public function testKeyExistsArrayAccess()
{
$array = new TraversableArrayAccessibleObject([

View File

@ -216,27 +216,6 @@ class ConsoleTest extends TestCase
$expectedHtml = "Error message. Here are some chars: < >\nError message. Here are even more chars: \"\"";
$this->assertEqualsWithoutLE($expectedHtml, Console::errorSummary($model, $options));
}
}
/**
* @property string name
* @property array types
* @property string description
*/
class TestConsoleModel extends DynamicModel
{
public function rules()
{
return [
['name', 'required'],
['name', 'string', 'max' => 100]
];
}
public function init()
{
$this->defineAttribute('name');
}
/**
* @covers \yii\helpers\BaseConsole::input()
@ -390,16 +369,16 @@ class TestConsoleModel extends DynamicModel
$this->truncateStreams();
foreach ([
'y' => true,
'Y' => true,
'yes' => true,
'YeS' => true,
'n' => false,
'N' => false,
'no' => false,
'NO' => false,
'WHAT?!' . PHP_EOL . 'yes' => true,
] as $currInput => $currAssertion) {
'y' => true,
'Y' => true,
'yes' => true,
'YeS' => true,
'n' => false,
'N' => false,
'no' => false,
'NO' => false,
'WHAT?!' . PHP_EOL . 'yes' => true,
] as $currInput => $currAssertion) {
$this->sendInput($currInput);
$result = ConsoleStub::confirm('Are you sure?');
$this->assertEquals($currAssertion, $result, $currInput);
@ -420,31 +399,63 @@ class TestConsoleModel extends DynamicModel
$this->sendInput('c');
$result = ConsoleStub::select('Usual behavior', $options);
$this->assertEquals('Usual behavior [c,d,m,?]: ', $this->readOutput());
$this->assertEquals('Usual behavior (c,d,m,?): ', $this->readOutput());
$this->assertEquals('c', $result);
$this->truncateStreams();
$this->sendInput('x', 'd');
$result = ConsoleStub::select('Wrong character', $options);
$this->assertEquals('Wrong character [c,d,m,?]: Wrong character [c,d,m,?]: ', $this->readOutput());
$this->assertEquals('Wrong character (c,d,m,?): Wrong character (c,d,m,?): ', $this->readOutput());
$this->assertEquals('d', $result);
$this->truncateStreams();
$this->sendInput('?', 'm');
$result = ConsoleStub::select('Using help', $options);
$this->assertEquals(
'Using help [c,d,m,?]: '
. ' c - cat'
. PHP_EOL
. ' d - dog'
. PHP_EOL
. ' m - mouse'
. PHP_EOL
. ' ? - Show help'
. PHP_EOL
. 'Using help [c,d,m,?]: ',
'Using help (c,d,m,?): '
. ' c - cat'
. PHP_EOL
. ' d - dog'
. PHP_EOL
. ' m - mouse'
. PHP_EOL
. ' ? - Show help'
. PHP_EOL
. 'Using help (c,d,m,?): ',
$this->readOutput()
);
$this->truncateStreams();
$this->sendInput('');
$result = ConsoleStub::select('Use Default', $options, 'm');
$this->assertEquals('m', $result);
$this->truncateStreams();
$this->sendInput('', 'd');
$result = ConsoleStub::select('Empty without Default', $options);
$this->assertEquals('Empty without Default (c,d,m,?): Empty without Default (c,d,m,?): ', $this->readOutput());
$this->assertEquals('d', $result);
$this->truncateStreams();
}
}
/**
* @property string name
* @property array types
* @property string description
*/
class TestConsoleModel extends DynamicModel
{
public function rules()
{
return [
['name', 'required'],
['name', 'string', 'max' => 100]
];
}
public function init()
{
$this->defineAttribute('name');
}
}

View File

@ -1270,4 +1270,60 @@ class FileHelperTest extends TestCase
[true, null, 'test'],
];
}
/**
* @dataProvider getExtensionsByMimeTypeProvider
* @param string $mimeType
* @param array $extensions
* @return void
*/
public function testGetExtensionsByMimeType($mimeType, $extensions)
{
$this->assertEquals($extensions, FileHelper::getExtensionsByMimeType($mimeType));
}
public function getExtensionsByMimeTypeProvider()
{
return [
[
'application/json',
[
'json',
],
],
[
'image/jpeg',
[ // Note: For backwards compatibility the (alphabetic) order of `framework/helpers/mimeTypes.php` is expected.
'jfif',
'jpe',
'jpeg',
'jpg',
'pjp',
'pjpeg',
],
],
];
}
/**
* @dataProvider getExtensionByMimeTypeProvider
* @param string $mimeType
* @param bool $preferShort
* @param array $extension
* @return void
*/
public function testGetExtensionByMimeType($mimeType, $preferShort, $extension)
{
$this->assertEquals($extension, FileHelper::getExtensionByMimeType($mimeType, $preferShort));
}
public function getExtensionByMimeTypeProvider()
{
return [
['application/json', true, 'json'],
['application/json', false, 'json'],
['image/jpeg', true, 'jpg'],
['image/jpeg', false, 'jpeg'],
];
}
}

View File

@ -39,8 +39,8 @@ class FormatConverterTest extends TestCase
public function testIntlIcuToPhpShortForm()
{
$this->assertEquals('n/j/y', FormatConverter::convertDateIcuToPhp('short', 'date', 'en-US'));
$this->assertEquals('d.m.y', FormatConverter::convertDateIcuToPhp('short', 'date', 'de-DE'));
$this->assertEqualsAnyWhitespace('n/j/y', FormatConverter::convertDateIcuToPhp('short', 'date', 'en-US'));
$this->assertEqualsAnyWhitespace('d.m.y', FormatConverter::convertDateIcuToPhp('short', 'date', 'de-DE'));
}
public function testIntlIcuToPhpShortFormDefaultLang()
@ -53,13 +53,13 @@ class FormatConverterTest extends TestCase
public function testIntlIcuToPhpShortFormTime()
{
$this->assertEquals('g:i A', FormatConverter::convertDateIcuToPhp('short', 'time', 'en-US'));
$this->assertEquals('H:i', FormatConverter::convertDateIcuToPhp('short', 'time', 'de-DE'));
$this->assertEqualsAnyWhitespace('g:i A', FormatConverter::convertDateIcuToPhp('short', 'time', 'en-US'));
$this->assertEqualsAnyWhitespace('H:i', FormatConverter::convertDateIcuToPhp('short', 'time', 'de-DE'));
}
public function testIntlIcuToPhpShortFormDateTime()
{
$this->assertEquals('n/j/y, g:i A', FormatConverter::convertDateIcuToPhp('short', 'datetime', 'en-US'));
$this->assertEqualsAnyWhitespace('n/j/y, g:i A', FormatConverter::convertDateIcuToPhp('short', 'datetime', 'en-US'));
$this->assertEquals(
PHP_VERSION_ID < 50600 ? 'd.m.y H:i' : 'd.m.y, H:i',
FormatConverter::convertDateIcuToPhp('short', 'datetime', 'de-DE')
@ -208,13 +208,13 @@ class FormatConverterTest extends TestCase
public function testIntlIcuToJuiShortFormTime()
{
$this->assertEquals(': ', FormatConverter::convertDateIcuToJui('short', 'time', 'en-US'));
$this->assertEquals(':', FormatConverter::convertDateIcuToJui('short', 'time', 'de-DE'));
$this->assertEqualsAnyWhitespace(': ', FormatConverter::convertDateIcuToJui('short', 'time', 'en-US'));
$this->assertEqualsAnyWhitespace(':', FormatConverter::convertDateIcuToJui('short', 'time', 'de-DE'));
}
public function testIntlIcuToJuiShortFormDateTime()
{
$this->assertEquals('m/d/y, : ', FormatConverter::convertDateIcuToJui('short', 'datetime', 'en-US'));
$this->assertEqualsAnyWhitespace('m/d/y, : ', FormatConverter::convertDateIcuToJui('short', 'datetime', 'en-US'));
$this->assertEquals(
PHP_VERSION_ID < 50600 ? 'dd.mm.y :' : 'dd.mm.y, :',
FormatConverter::convertDateIcuToJui('short', 'datetime', 'de-DE')

View File

@ -96,14 +96,18 @@ class JsonTest extends TestCase
$data->data = (object) null;
$this->assertSame('{}', Json::encode($data));
// Generator
$data = function () {
foreach (['a' => 1, 'b' => 2] as $name => $value) {
yield $name => $value;
}
};
$this->assertSame('{"a":1,"b":2}', Json::encode($data()));
// Generator (Only supported since PHP 5.5)
if (PHP_VERSION_ID >= 50500) {
$data = eval(<<<'PHP'
return function () {
foreach (['a' => 1, 'b' => 2] as $name => $value) {
yield $name => $value;
}
};
PHP
);
$this->assertSame('{"a":1,"b":2}', Json::encode($data()));
}
}
public function testHtmlEncode()

File diff suppressed because it is too large Load Diff

View File

@ -143,23 +143,23 @@ class FormatterDateTest extends TestCase
public function testAsTime()
{
$value = time();
$this->assertSame(date('g:i:s A', $value), $this->formatter->asTime($value));
$this->assertSameAnyWhitespace(date('g:i:s A', $value), $this->formatter->asTime($value));
$this->assertSame(date('h:i:s A', $value), $this->formatter->asTime($value, 'php:h:i:s A'));
$value = new DateTime();
$this->assertSame(date('g:i:s A', $value->getTimestamp()), $this->formatter->asTime($value));
$this->assertSameAnyWhitespace(date('g:i:s A', $value->getTimestamp()), $this->formatter->asTime($value));
$this->assertSame(date('h:i:s A', $value->getTimestamp()), $this->formatter->asTime($value, 'php:h:i:s A'));
if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
$value = new \DateTimeImmutable();
$this->assertSame(date('g:i:s A', $value->getTimestamp()), $this->formatter->asTime($value));
$this->assertSameAnyWhitespace(date('g:i:s A', $value->getTimestamp()), $this->formatter->asTime($value));
$this->assertSame(date('h:i:s A', $value->getTimestamp()), $this->formatter->asTime($value, 'php:h:i:s A'));
}
// empty input
$this->assertSame('12:00:00 AM', $this->formatter->asTime(''));
$this->assertSame('12:00:00 AM', $this->formatter->asTime(0));
$this->assertSame('12:00:00 AM', $this->formatter->asTime(false));
$this->assertSameAnyWhitespace('12:00:00 AM', $this->formatter->asTime(''));
$this->assertSameAnyWhitespace('12:00:00 AM', $this->formatter->asTime(0));
$this->assertSameAnyWhitespace('12:00:00 AM', $this->formatter->asTime(false));
// null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asTime(null));
}
@ -178,23 +178,35 @@ class FormatterDateTest extends TestCase
public function testAsDatetime()
{
$value = time();
$this->assertRegExp(date('~M j, Y,? g:i:s A~', $value), $this->formatter->asDatetime($value));
$this->assertRegExp(
$this->sanitizeWhitespaces(date('~M j, Y,? g:i:s A~', $value)),
$this->sanitizeWhitespaces($this->formatter->asDatetime($value))
);
$this->assertSame(date('Y/m/d h:i:s A', $value), $this->formatter->asDatetime($value, 'php:Y/m/d h:i:s A'));
$value = new DateTime();
$this->assertRegExp(date('~M j, Y,? g:i:s A~', $value->getTimestamp()), $this->formatter->asDatetime($value));
$this->assertRegExp(
$this->sanitizeWhitespaces(date('~M j, Y,? g:i:s A~', $value->getTimestamp())),
$this->sanitizeWhitespaces($this->formatter->asDatetime($value))
);
$this->assertSame(date('Y/m/d h:i:s A', $value->getTimestamp()), $this->formatter->asDatetime($value, 'php:Y/m/d h:i:s A'));
// empty time
$value = new DateTime();
$date = $value->format('Y-m-d');
$value = new DateTime($date);
$this->assertRegExp(date('~M j, Y,? g:i:s A~', $value->getTimestamp()), $this->formatter->asDatetime($date));
$this->assertRegExp(
$this->sanitizeWhitespaces(date('~M j, Y,? g:i:s A~', $value->getTimestamp())),
$this->sanitizeWhitespaces($this->formatter->asDatetime($date))
);
$this->assertSame(date('Y/m/d h:i:s A', $value->getTimestamp()), $this->formatter->asDatetime($date, 'php:Y/m/d h:i:s A'));
if (PHP_VERSION_ID >= 50500) {
$value = new \DateTimeImmutable();
$this->assertRegExp(date('~M j, Y,? g:i:s A~', $value->getTimestamp()), $this->formatter->asDatetime($value));
$this->assertRegExp(
$this->sanitizeWhitespaces(date('~M j, Y,? g:i:s A~', $value->getTimestamp())),
$this->sanitizeWhitespaces($this->formatter->asDatetime($value))
);
$this->assertSame(date('Y/m/d h:i:s A', $value->getTimestamp()), $this->formatter->asDatetime($value, 'php:Y/m/d h:i:s A'));
}
@ -205,9 +217,18 @@ class FormatterDateTest extends TestCase
}
// empty input
$this->assertRegExp('~Jan 1, 1970,? 12:00:00 AM~', $this->formatter->asDatetime(''));
$this->assertRegExp('~Jan 1, 1970,? 12:00:00 AM~', $this->formatter->asDatetime(0));
$this->assertRegExp('~Jan 1, 1970,? 12:00:00 AM~', $this->formatter->asDatetime(false));
$this->assertRegExp(
$this->sanitizeWhitespaces('~Jan 1, 1970,? 12:00:00 AM~'),
$this->sanitizeWhitespaces($this->formatter->asDatetime(''))
);
$this->assertRegExp(
$this->sanitizeWhitespaces('~Jan 1, 1970,? 12:00:00 AM~'),
$this->sanitizeWhitespaces($this->formatter->asDatetime(0))
);
$this->assertRegExp(
$this->sanitizeWhitespaces('~Jan 1, 1970,? 12:00:00 AM~'),
$this->sanitizeWhitespaces($this->formatter->asDatetime(false))
);
// null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null));
}

View File

@ -12,6 +12,7 @@ use yii\helpers\FileHelper;
use yii\log\Dispatcher;
use yii\log\FileTarget;
use yii\log\Logger;
use yiiunit\framework\log\mocks\CustomLogger;
use yiiunit\TestCase;
/**
@ -110,4 +111,53 @@ class FileTargetTest extends TestCase
$this->assertFileNotExists($logFile . '.3');
$this->assertFileNotExists($logFile . '.4');
}
public function testLogEmptyStrings()
{
$logFile = Yii::getAlias('@yiiunit/runtime/log/filetargettest.log');
$this->clearLogFile($logFile);
$logger = new CustomLogger();
$logger->logFile = $logFile;
$logger->messages = array_fill(0, 1, 'xxx');
$logger->export();
$test = file($logFile);
$this->assertEquals("xxx\n", $test[0]);
$this->clearLogFile($logFile);
$logger = new CustomLogger();
$logger->logFile = $logFile;
$logger->messages = array_fill(0, 3, 'xxx');
$logger->export();
$test = file($logFile);
$this->assertEquals("xxx\n", $test[0]);
$this->assertEquals("xxx\n", $test[1]);
$this->assertEquals("xxx\n", $test[2]);
$this->clearLogFile($logFile);
$logger->messages = array_fill(0, 1, 'yyy');
$logger->export();
$this->assertFileNotExists($logFile);
$logger->messages = array_fill(0, 10, '');
$logger->export();
$this->assertFileNotExists($logFile);
$logger->messages = array_fill(0, 10, null);
$logger->export();
$this->assertFileNotExists($logFile);
}
private function clearLogFile($logFile)
{
FileHelper::removeDirectory(dirname($logFile));
mkdir(dirname($logFile), 0777, true);
}
}

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