diff --git a/.appveyor.yml b/.appveyor.yml index ee07f29..8b529e4 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,6 +1,5 @@ build: false version: dev-{build} -shallow_clone: true clone_folder: C:\projects\yii2 environment: diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c0c1cac..d6f1e3b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,5 @@ # These are supported funding model platforms open_collective: yiisoft +github: [yiisoft] +tidelift: "packagist/yiisoft/yii2" diff --git a/.github/SECURITY.md b/.github/SECURITY.md index f713847..405acca 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -3,4 +3,4 @@ Please use the [security issue form](https://www.yiiframework.com/security) to report to us any security issue you find in Yii. DO NOT use the issue tracker or discuss it in the public forum as it will cause more damage than help. -Please note that as a non-commerial OpenSource project we are not able to pay bounties at the moment. +Please note that as a non-commercial OpenSource project we are not able to pay bounties at the moment. diff --git a/.github/move.yml b/.github/move.yml deleted file mode 100644 index 1589712..0000000 --- a/.github/move.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Configuration for move-issues - https://github.com/dessant/move-issues - -# Delete the command comment when it contains no other content -deleteCommand: true - -# Close the source issue after moving -closeSourceIssue: true - -# Lock the source issue after moving -lockSourceIssue: false - -# Mention issue and comment authors -mentionAuthors: true - -# Preserve mentions in the issue content -keepContentMentions: false - -# Set custom aliases for targets -# aliases: -# r: repo -# or: owner/repo - -# Repository to extend settings from -# _extends: repo diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ed61ec1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,113 @@ +name: build + +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 + XDEBUG_MODE: coverage, develop + +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'] + + steps: + - name: Generate french locale + run: sudo locale-gen fr_FR.UTF-8 + - name: Checkout + uses: actions/checkout@v2 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + 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 + uses: niden/actions-memcached@v7 + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache composer dependencies + uses: actions/cache@v1 + 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: PHP Unit tests for PHP 7.1 + run: vendor/bin/phpunit --verbose --coverage-clover=coverage.clover --exclude-group $PHPUNIT_EXCLUDE_GROUP --colors=always + if: matrix.php == '7.1' + - name: PHP Unit tests for PHP >= 7.2 + run: vendor/bin/phpunit --verbose --exclude-group $PHPUNIT_EXCLUDE_GROUP --colors=always + env: + PHPUNIT_EXCLUDE_GROUP: oci,wincache,xcache,zenddata,cubrid + if: matrix.php >= '7.2' + - name: PHP Unit tests for PHP <= 7.0 + run: vendor/bin/phpunit --verbose --exclude-group $PHPUNIT_EXCLUDE_GROUP --colors=always + if: matrix.php <= '7.0' + - 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 + + npm: + name: NPM 6 on ubuntu-latest + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.2 + ini-values: session.save_path=${{ runner.temp }} + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache composer dependencies + uses: actions/cache@v1 + 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 diff --git a/.github/workflows/ci-mssql.yml b/.github/workflows/ci-mssql.yml new file mode 100644 index 0000000..232eb29 --- /dev/null +++ b/.github/workflows/ci-mssql.yml @@ -0,0 +1,103 @@ +on: + - pull_request + - push + +name: ci-mssql + +jobs: + tests: + name: PHP ${{ matrix.php }}-mssql-${{ matrix.mssql }} + + env: + key: cache + + 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' + + services: + mssql: + image: mcr.microsoft.com/mssql/${{ matrix.mssql }} + env: + SA_PASSWORD: YourStrong!Passw0rd + ACCEPT_EULA: Y + MSSQL_PID: Developer + ports: + - 1433:1433 + options: --name=mssql --health-cmd="/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1'" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Create MS SQL Database + run: docker exec -i mssql /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'CREATE DATABASE yiitest' + + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: ${{ matrix.extensions }} + ini-values: date.timezone='UTC' + 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@v2 + 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: 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 + 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 diff --git a/.github/workflows/ci-mysql.yml b/.github/workflows/ci-mysql.yml new file mode 100644 index 0000000..de0a5e1 --- /dev/null +++ b/.github/workflows/ci-mysql.yml @@ -0,0 +1,80 @@ +on: + - pull_request + - push + +name: ci-mysql + +jobs: + tests: + name: PHP ${{ matrix.php-version }}-mysql-${{ matrix.mysql-version }} + env: + extensions: curl, intl, pdo, pdo_mysql + key: cache-v1 + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php-version: + - "7.4" + + mysql-version: + - "latest" + + services: + mysql: + image: mysql:${{ matrix.mysql-version }} + 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 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - 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 }} + ini-values: date.timezone='UTC' + coverage: pcov + + - 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 + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run mysql tests with phpunit + run: vendor/bin/phpunit --group mysql --colors=always diff --git a/.github/workflows/ci-oracle.yml b/.github/workflows/ci-oracle.yml new file mode 100644 index 0000000..df7bb3f --- /dev/null +++ b/.github/workflows/ci-oracle.yml @@ -0,0 +1,91 @@ +on: + - pull_request + - push + +name: ci-oracle + +jobs: + tests: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + env: + extensions: oci8, pdo, pdo_oci + key: cache-v1 + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "7.4" + + services: + oci: + image: wnameless/oracle-xe-11g-r2:latest + ports: + - 1521:1521 + options: --name=oci + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - 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 + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: ${{ env.extensions }} + ini-values: date.timezone='UTC' + coverage: pcov + tools: composer:v2, pecl + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v2 + 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 php 7.4 + if: matrix.php == '7.4' + 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: PHP Unit tests for PHP 7.4 + run: vendor/bin/phpunit --coverage-clover=coverage.clover --group oci --colors=always + if: matrix.php == '7.4' + + - name: Run tests with phpunit without coverage + run: vendor/bin/phpunit --group oci --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.4' + continue-on-error: true # if is fork diff --git a/.github/workflows/ci-pgsql.yml b/.github/workflows/ci-pgsql.yml new file mode 100644 index 0000000..b934f6a --- /dev/null +++ b/.github/workflows/ci-pgsql.yml @@ -0,0 +1,84 @@ +on: + - pull_request + - push + +name: ci-pgsql + +jobs: + tests: + name: PHP ${{ matrix.php-version }}-pgsql-${{ matrix.pgsql-version }} + env: + extensions: curl, intl, pdo, pdo_pgsql + key: cache-v1 + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php-version: + - "7.4" + + pgsql-version: + - "10" + - "11" + - "12" + - "13" + + services: + postgres: + image: postgres:${{ matrix.pgsql-version }} + 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 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - 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 }} + ini-values: date.timezone='UTC' + coverage: pcov + + - 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 + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run pgsql tests with phpunit + run: vendor/bin/phpunit --group pgsql --colors=always diff --git a/.gitignore b/.gitignore index f941e24..769d405 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # phpstorm project files .idea +*.iml # netbeans project files nbproject diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a0f20c4..8b01aa0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,13 +4,12 @@ services: - docker:dind variables: - DOCKER_YII2_PHP_IMAGE: yiisoftware/yii2-php:7.1-apache + DOCKER_YII2_PHP_IMAGE: yiisoftware/yii2-php:7.4-apache DOCKER_MYSQL_IMAGE: percona:5.7 DOCKER_POSTGRES_IMAGE: postgres:9.3 before_script: - - apk add --no-cache python py2-pip git - - pip install --no-cache-dir docker-compose==1.16.0 + - apk add --no-cache git curl docker-compose - docker info - cd tests diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 44ad97a..5279ff4 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -16,4 +16,5 @@ filter: tools: external_code_coverage: + runs: 3 timeout: 2100 # Timeout in seconds. diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 03ff050..0000000 --- a/.travis.yml +++ /dev/null @@ -1,223 +0,0 @@ -dist: xenial - -# faster builds on new travis setup not using sudo -# temporary disable, see https://github.com/travis-ci/travis-ci/issues/6842 -#sudo: false -sudo: required -group: edge - -# build only on master branches -# commented as this prevents people from running builds on their forks: -# https://github.com/yiisoft/yii2/commit/bd87be990fa238c6d5e326d0a171f38d02dc253a -#branches: -# only: -# - master -# - 2.1 - - -# -# Test Matrix -# - -language: php - -env: - global: - - DEFAULT_COMPOSER_FLAGS="--prefer-dist --no-interaction --no-progress --optimize-autoloader" - - TASK_TESTS_PHP=1 - - TASK_TESTS_JS=0 - - TASK_TESTS_COVERAGE=0 - - TRAVIS_SECOND_USER=travis_two - - PHPUNIT_EXCLUDE_GROUP=mssql,oci,wincache,xcache,zenddata,cubrid - - -services: - - memcached - - postgresql - - docker - -# cache vendor dirs -cache: - directories: - - vendor - - $HOME/.composer/cache - - $HOME/.npm - -# try running against postgres 9.6 -addons: - postgresql: "9.6" - code_climate: - repo_token: 2935307212620b0e2228ab67eadd92c9f5501ddb60549d0d86007a354d56915b - -matrix: - fast_finish: true - include: - - php: "7.3" - env: PHPUNIT_EXCLUDE_GROUP=oci,wincache,xcache,zenddata,cubrid - - - php: "7.2" - env: PHPUNIT_EXCLUDE_GROUP=oci,wincache,xcache,zenddata,cubrid - - # run tests coverage on PHP 7.1 - - php: "7.1" - env: - - TASK_TESTS_COVERAGE=1 - - PHPUNIT_EXCLUDE_GROUP=oci,wincache,xcache,zenddata,cubrid - - - php: "7.0" - - - php: "5.6" - - - php: "5.5" - dist: trusty - - - php: "5.4" - dist: trusty - - # Test against HHVM 3.21 LTS version by using trusty - - php: hhvm-3.21 - sudo: true - addons: - code_climate: - repo_token: 2935307212620b0e2228ab67eadd92c9f5501ddb60549d0d86007a354d56915b - postgresql: "9.6" - services: - - mysql - - postgresql - - # test against the latest pre 3.26 HHVM version by using a newer image. - # @see https://github.com/facebook/hhvm/issues/8192 - - php: hhvm-3.24 - sudo: true - addons: - code_climate: - repo_token: 2935307212620b0e2228ab67eadd92c9f5501ddb60549d0d86007a354d56915b - postgresql: "9.6" - services: - - mysql - - postgresql - - - php: nightly - services: - - mysql - - postgresql - - # have a separate branch for javascript tests - - language: node_js - node_js: "6" - env: TASK_TESTS_PHP=0 TASK_TESTS_JS=1 - # overwrite services used for PHP tests - services: - - allow_failures: - - php: nightly - - php: hhvm-3.21 - - php: hhvm-3.24 - -install: - - | - if [[ $TASK_TESTS_COVERAGE != 1 && $TRAVIS_PHP_VERSION != hhv* ]]; then - # disable xdebug for performance reasons when code coverage is not needed. note: xdebug on hhvm is disabled by default - phpenv config-rm xdebug.ini || echo "xdebug is not installed" - fi - - # install composer dependencies - - travis_retry composer self-update - - export PATH="$HOME/.composer/vendor/bin:$PATH" - - travis_retry composer install $DEFAULT_COMPOSER_FLAGS - - # setup PHP extension - - | - if [[ $TASK_TESTS_PHP == 1 && $TRAVIS_PHP_VERSION != nightly ]]; then - tests/data/travis/apc-setup.sh - tests/data/travis/memcache-setup.sh - tests/data/travis/imagick-setup.sh - source tests/data/travis/mysql-setup.sh - source tests/data/travis/mssql-setup.sh - fi - - # setup JS test - - | - if [ $TASK_TESTS_JS == 1 ]; then - travis_retry npm install - fi - - # Needed for FileCacheTest - - sudo useradd $TRAVIS_SECOND_USER --gid $(id -g) -M - -before_script: - # - # Disable: - # 1) the HHVM JIT for faster testing; - # 2) the session GC for testing stability. - # - # The second allows to avoid accidental unpredictable failings with message: - # `ps_files_cleanup_dir: opendir(/var/lib/hhvm/sessions) failed: Permission denied (13)` - # - - if [[ $TRAVIS_PHP_VERSION = hhv* ]]; then - echo 'hhvm.jit = 0' >> /etc/hhvm/php.ini; - echo 'session.gc_probability = 0' >> /etc/hhvm/php.ini; - fi - - # show some versions and env information - - php --version - - composer --version - - | - if [ $TASK_TESTS_PHP == 1 ]; then - php -r "echo INTL_ICU_VERSION . \"\n\";" - php -r "echo INTL_ICU_DATA_VERSION . \"\n\";" - psql --version - mysql --version - sudo mysql_upgrade || echo "MySQL is already up to date" - fi - - | - if [ $TASK_TESTS_JS == 1 ]; then - node --version - npm --version - fi - - # initialize databases - - | - if [ $TASK_TESTS_PHP == 1 ]; then - travis_retry mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE `yiitest`;'; - mysql -h 127.0.0.1 -u root -proot -e "SET GLOBAL sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';"; - mysql -h 127.0.0.1 -u root -proot -e "CREATE USER 'travis'@'localhost' IDENTIFIED WITH mysql_native_password;"; - mysql -h 127.0.0.1 -u root -proot -e "GRANT ALL PRIVILEGES ON *.* TO 'travis'@'localhost' WITH GRANT OPTION;"; - psql -U postgres -c 'CREATE DATABASE yiitest;'; - fi - - # enable code coverage - - | - if [ $TASK_TESTS_COVERAGE == 1 ]; then - PHPUNIT_FLAGS="--coverage-clover=coverage.clover" - fi - - # Disable DEPRECATE messages during PHPUnit initialization on PHP 7.2. To fix them, PHPUnit should be updated to 6.* - # For Yii2 tests, messages will be enabled by tests/bootstrap.php - - | - if [[ $TRAVIS_PHP_VERSION == 7.2 || $TRAVIS_PHP_VERSION == 7.3 || $TRAVIS_PHP_VERSION = nightly ]]; then - echo 'Disabled DEPRECATED notifications for PHP >= 7.2'; - echo 'error_reporting = E_ALL & ~E_DEPRECATED' >> /tmp/php-config.ini; - phpenv config-add /tmp/php-config.ini; - fi - - -script: - # PHP tests - - | - if [ $TASK_TESTS_PHP == 1 ]; then - vendor/bin/phpunit --verbose $PHPUNIT_FLAGS --exclude-group $PHPUNIT_EXCLUDE_GROUP - fi - - # JS tests - - | - if [ $TASK_TESTS_JS == 1 ]; then - npm test - fi - -after_script: - - | - if [ $TASK_TESTS_COVERAGE == 1 ]; then - travis_retry wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.clover - fi diff --git a/Dockerfile b/Dockerfile index 77ffdfd..86f3a3a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,10 @@ 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 RUN /usr/local/bin/composer install --prefer-dist ADD ./ /project ENV PATH /project/vendor/bin:${PATH} diff --git a/README.md b/README.md index d2712aa..53b565a 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,9 @@ The framework is easy to adjust to meet your needs, because Yii has been designe [![Latest Stable Version](https://img.shields.io/packagist/v/yiisoft/yii2.svg)](https://packagist.org/packages/yiisoft/yii2) [![Total Downloads](https://img.shields.io/packagist/dt/yiisoft/yii2.svg)](https://packagist.org/packages/yiisoft/yii2) -[![Build Status](https://img.shields.io/travis/yiisoft/yii2.svg)](https://travis-ci.org/yiisoft/yii2) +[![Build Status](https://github.com/yiisoft/yii2/workflows/build/badge.svg)](https://github.com/yiisoft/yii2/actions) [![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/yii2/badges/coverage.png?s=31d80f1036099e9d6a3e4d7738f6b000b3c3d10e)](https://scrutinizer-ci.com/g/yiisoft/yii2/) [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/yiisoft/yii2/badges/quality-score.png?s=b1074a1ff6d0b214d54fa5ab7abbb90fc092471d)](https://scrutinizer-ci.com/g/yiisoft/yii2/) -[![Code Climate](https://img.shields.io/codeclimate/github/yiisoft/yii2.svg)](https://codeclimate.com/github/yiisoft/yii2) Installation ------------ diff --git a/build/controllers/DevController.php b/build/controllers/DevController.php index 45b0557..5cb1b32 100644 --- a/build/controllers/DevController.php +++ b/build/controllers/DevController.php @@ -51,6 +51,7 @@ class DevController extends Controller 'apidoc' => 'git@github.com:yiisoft/yii2-apidoc.git', 'authclient' => 'git@github.com:yiisoft/yii2-authclient.git', 'bootstrap' => 'git@github.com:yiisoft/yii2-bootstrap.git', + 'bootstrap4' => 'git@github.com:yiisoft/yii2-bootstrap4.git', 'codeception' => 'git@github.com:yiisoft/yii2-codeception.git', 'composer' => 'git@github.com:yiisoft/yii2-composer.git', 'debug' => 'git@github.com:yiisoft/yii2-debug.git', @@ -334,7 +335,7 @@ class DevController extends Controller continue; } // ignore hidden directories - if ($file[0] === '.') { + if (strpos($file, '.') === 0) { continue; } if (is_dir("$dir/$file")) { diff --git a/build/controllers/MimeTypeController.php b/build/controllers/MimeTypeController.php index 5645e88..5b03491 100644 --- a/build/controllers/MimeTypeController.php +++ b/build/controllers/MimeTypeController.php @@ -85,7 +85,7 @@ class MimeTypeController extends Controller $mimeMap = []; foreach (explode("\n", $content) as $line) { $line = trim($line); - if (empty($line) || $line[0] === '#') { // skip comments and empty lines + if (empty($line) || strpos($line, '#') === 0) { // skip comments and empty lines continue; } $parts = preg_split('/\s+/', $line); diff --git a/build/controllers/PhpDocController.php b/build/controllers/PhpDocController.php index 7d2f41d..144413f 100644 --- a/build/controllers/PhpDocController.php +++ b/build/controllers/PhpDocController.php @@ -329,16 +329,16 @@ class PhpDocController extends Controller $tag = false; } elseif ($docBlock) { $line = ltrim($line); - if (isset($line[0]) && $line[0] === '*') { + if (strpos($line, '*') === 0) { $line = substr($line, 1); } - if (isset($line[0]) && $line[0] === ' ') { + if (strpos($line, ' ') === 0) { $line = substr($line, 1); } $docLine = str_replace("\t", ' ', rtrim($line)); if (empty($docLine)) { $listIndent = ''; - } elseif ($docLine[0] === '@') { + } elseif (strpos($docLine, '@') === 0) { $listIndent = ''; $codeBlock = false; $tag = true; @@ -453,15 +453,15 @@ class PhpDocController extends Controller $endofPrivate = $i; $property = 'Private'; $level = 0; - } elseif (substr($line, 0, 6) === 'const ') { + } elseif (strpos($line, 'const ') === 0) { $endofConst = $i; $property = false; - } elseif (substr($line, 0, 4) === 'use ') { + } elseif (strpos($line, 'use ') === 0) { $endofUse = $i; $property = false; - } elseif (!empty($line) && $line[0] === '*') { + } elseif (strpos($line, '*') === 0) { $property = false; - } elseif (!empty($line) && $line[0] !== '*' && strpos($line, 'function ') !== false || $line === '}') { + } elseif (strpos($line, '*') !== 0 && strpos($line, 'function ') !== false || $line === '}') { break; } @@ -504,11 +504,19 @@ class PhpDocController extends Controller protected function updateClassPropertyDocs($file, $className, $propertyDoc) { + if ($this->shouldSkipClass($className)) { + $this->stderr("[INFO] Skipping class $className.\n", Console::FG_BLUE, Console::BOLD); + return false; + } + try { $ref = new \ReflectionClass($className); } catch (\Exception $e) { $this->stderr("[ERR] Unable to create ReflectionClass for class '$className': " . $e->getMessage() . "\n", Console::FG_RED); return false; + } catch (\Error $e) { + $this->stderr("[ERR] Unable to create ReflectionClass for class '$className': " . $e->getMessage() . "\n", Console::FG_RED); + return false; } if ($ref->getFileName() != $file) { $this->stderr("[ERR] Unable to create ReflectionClass for class: $className loaded class is not from file: $file\n", Console::FG_RED); @@ -612,9 +620,9 @@ class PhpDocController extends Controller $propertyPosition = false; foreach ($lines as $i => $line) { $line = trim($line); - if (strncmp($line, '* @property ', 12) === 0) { + if (strncmp($line, '* @property', 11) === 0) { $propertyPart = true; - } elseif ($propertyPart && $line == '*') { + } elseif ($propertyPart && $line === '*') { $propertyPosition = $i; $propertyPart = false; } @@ -627,7 +635,7 @@ class PhpDocController extends Controller } } - // if no properties or other tags where present add properties at the end + // if no properties or other tags were present add properties at the end if ($propertyPosition === false) { $propertyPosition = \count($lines) - 2; } @@ -649,8 +657,12 @@ class PhpDocController extends Controller $file = str_replace("\r", '', str_replace("\t", ' ', file_get_contents($fileName, true))); $ns = $this->match('#\nnamespace (?[\w\\\\]+);\n#', $file); $namespace = reset($ns); - $namespace = $namespace['name']; - $classes = $this->match('#\n(?:abstract )(?:final )?class (?\w+)( extends .+)?( implements .+)?\n\{(?.*)\n\}(\n|$)#', $file); + if ($namespace === false) { + $namespace = '\\'; + } else { + $namespace = $namespace['name']; + } + $classes = $this->match('#\n(?:abstract )?(?:final )?class (?\w+)( extends .+)?( implements .+)?\n\{(?.*)\n\}(\n|$)#', $file); if (\count($classes) > 1) { $this->stderr("[ERR] There should be only one class in a file: $fileName\n", Console::FG_RED); @@ -710,43 +722,45 @@ class PhpDocController extends Controller ]; } + if (\count($props) === 0) { + continue; + } + ksort($props); - if (\count($props) > 0) { - $phpdoc .= " *\n"; - foreach ($props as $propName => &$prop) { - $docline = ' * @'; - $docline .= 'property'; // Do not use property-read and property-write as few IDEs support complex syntax. - $note = ''; - if (isset($prop['get'], $prop['set'])) { - if ($prop['get']['type'] != $prop['set']['type']) { - $note = ' Note that the type of this property differs in getter and setter.' - . ' See [[get' . ucfirst($propName) . '()]] and [[set' . ucfirst($propName) . '()]] for details.'; - } - } elseif (isset($prop['get'])) { - if (!$this->hasSetterInParents($className, $propName)) { - $note = ' This property is read-only.'; - //$docline .= '-read'; - } - } elseif (isset($prop['set'])) { - if (!$this->hasGetterInParents($className, $propName)) { - $note = ' This property is write-only.'; - //$docline .= '-write'; - } - } else { - continue; + $phpdoc .= " *\n"; + foreach ($props as $propName => &$prop) { + $docLine = ' * @property'; + $note = ''; + if (isset($prop['get'], $prop['set'])) { + if ($prop['get']['type'] != $prop['set']['type']) { + $note = ' Note that the type of this property differs in getter and setter.' + . ' See [[get' . ucfirst($propName) . '()]]' + . ' and [[set' . ucfirst($propName) . '()]] for details.'; } - $docline .= ' ' . $this->getPropParam($prop, 'type') . " $$propName "; - $comment = explode("\n", $this->getPropParam($prop, 'comment') . $note); - foreach ($comment as &$cline) { - $cline = ltrim($cline, '* '); + } elseif (isset($prop['get'])) { + if (!$this->hasSetterInParents($className, $propName)) { + $note = ' This property is read-only.'; + $docLine .= '-read'; } - $docline = wordwrap($docline . implode(' ', $comment), 110, "\n * ") . "\n"; - - $phpdoc .= $docline; + } elseif (isset($prop['set'])) { + if (!$this->hasGetterInParents($className, $propName)) { + $note = ' This property is write-only.'; + $docLine .= '-write'; + } + } else { + continue; + } + $docLine .= ' ' . $this->getPropParam($prop, 'type') . " $$propName "; + $comment = explode("\n", $this->getPropParam($prop, 'comment') . $note); + foreach ($comment as &$cline) { + $cline = ltrim($cline, '* '); } - $phpdoc .= " *\n"; + $docLine = wordwrap($docLine . implode(' ', $comment), 110, "\n * ") . "\n"; + + $phpdoc .= $docLine; } + $phpdoc .= " *\n"; } return [$className, $phpdoc]; @@ -783,7 +797,7 @@ class PhpDocController extends Controller return ''; } - return strtoupper(substr($str, 0, 1)) . substr($str, 1) . ($str[\strlen($str) - 1] != '.' ? '.' : ''); + return strtoupper(substr($str, 0, 1)) . substr($str, 1) . ($str[\strlen($str) - 1] !== '.' ? '.' : ''); } protected function getPropParam($prop, $param) @@ -812,11 +826,17 @@ class PhpDocController extends Controller protected function hasGetterInParents($className, $propName) { $class = $className; - while ($parent = get_parent_class($class)) { - if (method_exists($parent, 'get' . ucfirst($propName))) { - return true; + + try { + while ($parent = get_parent_class($class)) { + if (method_exists($parent, 'get' . ucfirst($propName))) { + return true; + } + $class = $parent; } - $class = $parent; + } catch (\Throwable $t) { + $this->stderr("[ERR] Error when getting parents for $className\n", Console::FG_RED); + return false; } return false; } @@ -829,11 +849,17 @@ class PhpDocController extends Controller protected function hasSetterInParents($className, $propName) { $class = $className; - while ($parent = get_parent_class($class)) { - if (method_exists($parent, 'set' . ucfirst($propName))) { - return true; + + try { + while ($parent = get_parent_class($class)) { + if (method_exists($parent, 'set' . ucfirst($propName))) { + return true; + } + $class = $parent; } - $class = $parent; + } catch (\Throwable $t) { + $this->stderr("[ERR] Error when getting parents for $className\n", Console::FG_RED); + return false; } return false; } @@ -851,4 +877,12 @@ class PhpDocController extends Controller } return !$isDepreceatedObject && !$ref->isSubclassOf('yii\base\BaseObject') && $className !== 'yii\base\BaseObject'; } + + private function shouldSkipClass($className) + { + if (PHP_VERSION_ID > 70100) { + return $className === 'yii\base\Object'; + } + return false; + } } diff --git a/build/controllers/ReleaseController.php b/build/controllers/ReleaseController.php index 4f373aa..e3d1765 100644 --- a/build/controllers/ReleaseController.php +++ b/build/controllers/ReleaseController.php @@ -223,7 +223,7 @@ class ReleaseController extends Controller } $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.org/yiisoft/yii2$travisUrl/builds\n"); + $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"); $this->printWhatUrls($what, $versions); $this->stdout("\n"); diff --git a/build/controllers/TranslationController.php b/build/controllers/TranslationController.php index 0200ad9..b4d2164 100644 --- a/build/controllers/TranslationController.php +++ b/build/controllers/TranslationController.php @@ -126,11 +126,11 @@ class TranslationController extends Controller { $lines = explode("\n", $diff); foreach ($lines as $key => $val) { - if (mb_substr($val, 0, 1, 'utf-8') === '@') { + if (strpos($val, '@') === 0) { $lines[$key] = '' . Html::encode($val) . ''; - } elseif (mb_substr($val, 0, 1, 'utf-8') === '+') { + } elseif (strpos($val, '+') === 0) { $lines[$key] = '' . Html::encode($val) . ''; - } elseif (mb_substr($val, 0, 1, 'utf-8') === '-') { + } elseif (strpos($val, '-') === 0) { $lines[$key] = '' . Html::encode($val) . ''; } else { $lines[$key] = Html::encode($val); diff --git a/composer.json b/composer.json index 19cf2b8..ed87b7d 100644 --- a/composer.json +++ b/composer.json @@ -75,12 +75,13 @@ "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.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.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" }, "require-dev": { + "cweagans/composer-patches": "^1.7", "phpunit/phpunit": "4.8.34", "cebe/indent": "~1.0.2", "friendsofphp/php-cs-fixer": "~2.2.3", @@ -101,6 +102,11 @@ "yii\\cs\\": "cs/src/" } }, + "autoload-dev": { + "files": [ + "tests/bootstrap.php" + ] + }, "config": { "platform": {"php": "5.4"} }, @@ -110,6 +116,16 @@ "extra": { "branch-alias": { "dev-master": "2.0.x-dev" + }, + "composer-exit-on-patch-failure": true, + "patches": { + "phpunit/phpunit-mock-objects": { + "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" + }, + "phpunit/phpunit": { + "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", + "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch" + } } } } diff --git a/composer.lock b/composer.lock index 631f0e8..654dae1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,21 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "20618e7e02e835f9f1ee41b339f2b61a", + "content-hash": "6282c196c2380b0d30b7104eccef989a", "packages": [ { "name": "bower-asset/inputmask", "version": "3.3.11", "source": { "type": "git", - "url": "https://github.com/RobinHerbots/Inputmask.git", + "url": "git@github.com:RobinHerbots/Inputmask.git", "reference": "5e670ad62f50c738388d4dcec78d2888505ad77b" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/RobinHerbots/Inputmask/zipball/5e670ad62f50c738388d4dcec78d2888505ad77b", - "reference": "5e670ad62f50c738388d4dcec78d2888505ad77b", - "shasum": null + "reference": "5e670ad62f50c738388d4dcec78d2888505ad77b" }, "require": { "bower-asset/jquery": ">=1.7" @@ -30,17 +29,16 @@ }, { "name": "bower-asset/jquery", - "version": "3.2.1", + "version": "3.5.1", "source": { "type": "git", - "url": "https://github.com/jquery/jquery-dist.git", - "reference": "77d2a51d0520d2ee44173afdf4e40a9201f5964e" + "url": "git@github.com:jquery/jquery-dist.git", + "reference": "4c0e4becb8263bb5b3e6dadc448d8e7305ef8215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/77d2a51d0520d2ee44173afdf4e40a9201f5964e", - "reference": "77d2a51d0520d2ee44173afdf4e40a9201f5964e", - "shasum": null + "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/4c0e4becb8263bb5b3e6dadc448d8e7305ef8215", + "reference": "4c0e4becb8263bb5b3e6dadc448d8e7305ef8215" }, "type": "bower-asset", "license": [ @@ -58,24 +56,22 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", - "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", - "shasum": null + "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" }, "type": "bower-asset" }, { "name": "bower-asset/yii2-pjax", - "version": "2.0.7.1", + "version": "v2.0.7", "source": { "type": "git", - "url": "git@github.com:yiisoft/jquery-pjax.git", - "reference": "aef7b953107264f00234902a3880eb50dafc48be" + "url": "https://github.com/yiisoft/jquery-pjax.git", + "reference": "885fc8c2d36c93a801b6af0ee8ad55d79df97cb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/aef7b953107264f00234902a3880eb50dafc48be", - "reference": "aef7b953107264f00234902a3880eb50dafc48be", - "shasum": null + "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/885fc8c2d36c93a801b6af0ee8ad55d79df97cb1", + "reference": "885fc8c2d36c93a801b6af0ee8ad55d79df97cb1" }, "require": { "bower-asset/jquery": ">=1.8" @@ -87,16 +83,16 @@ }, { "name": "cebe/markdown", - "version": "1.1.2", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/cebe/markdown.git", - "reference": "25b28bae8a6f185b5030673af77b32e1163d5c6e" + "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cebe/markdown/zipball/25b28bae8a6f185b5030673af77b32e1163d5c6e", - "reference": "25b28bae8a6f185b5030673af77b32e1163d5c6e", + "url": "https://api.github.com/repos/cebe/markdown/zipball/9bac5e971dd391e2802dca5400bbeacbaea9eb86", + "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86", "shasum": "" }, "require": { @@ -114,7 +110,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -143,27 +139,31 @@ "markdown", "markdown-extra" ], - "time": "2017-07-16T21:13:23+00:00" + "support": { + "issues": "https://github.com/cebe/markdown/issues", + "source": "https://github.com/cebe/markdown" + }, + "time": "2018-03-26T11:24:36+00:00" }, { "name": "ezyang/htmlpurifier", - "version": "v4.9.3", + "version": "v4.13.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "95e1bae3182efc0f3422896a3236e991049dac69" + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/95e1bae3182efc0f3422896a3236e991049dac69", - "reference": "95e1bae3182efc0f3422896a3236e991049dac69", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75", "shasum": "" }, "require": { "php": ">=5.2" }, "require-dev": { - "simpletest/simpletest": "^1.1" + "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd" }, "type": "library", "autoload": { @@ -172,11 +172,14 @@ }, "files": [ "library/HTMLPurifier.composer.php" + ], + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL" + "LGPL-2.1-or-later" ], "authors": [ { @@ -190,27 +193,32 @@ "keywords": [ "html" ], - "time": "2017-06-03T02:28:16+00:00" + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/master" + }, + "time": "2020-06-29T00:56:53+00:00" }, { "name": "yiisoft/yii2-composer", - "version": "2.0.5", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-composer.git", - "reference": "3f4923c2bde6caf3f5b88cc22fdd5770f52f8df2" + "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/3f4923c2bde6caf3f5b88cc22fdd5770f52f8df2", - "reference": "3f4923c2bde6caf3f5b88cc22fdd5770f52f8df2", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/94bb3f66e779e2774f8776d6e1bdeab402940510", + "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0" + "composer-plugin-api": "^1.0 | ^2.0" }, "require-dev": { - "composer/composer": "^1.0" + "composer/composer": "^1.0 | ^2.0@dev", + "phpunit/phpunit": "<7" }, "type": "composer-plugin", "extra": { @@ -232,6 +240,10 @@ { "name": "Qiang Xue", "email": "qiang.xue@gmail.com" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc" } ], "description": "The composer plugin for Yii extension installer", @@ -240,7 +252,28 @@ "extension installer", "yii2" ], - "time": "2016-12-20T13:26:02+00:00" + "support": { + "forum": "http://www.yiiframework.com/forum/", + "irc": "irc://irc.freenode.net/yii", + "issues": "https://github.com/yiisoft/yii2-composer/issues", + "source": "https://github.com/yiisoft/yii2-composer", + "wiki": "http://www.yiiframework.com/wiki/" + }, + "funding": [ + { + "url": "https://github.com/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-composer", + "type": "tidelift" + } + ], + "time": "2020-06-24T00:04:01+00:00" } ], "packages-dev": [ @@ -275,28 +308,31 @@ } ], "description": "a small tool to convert text file indentation", + "support": { + "issues": "https://github.com/cebe/indent/issues", + "source": "https://github.com/cebe/indent/tree/master" + }, "time": "2014-05-23T14:40:08+00:00" }, { "name": "composer/semver", - "version": "1.4.2", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" + "reference": "38276325bd896f90dfcfe30029aa5db40df387a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", + "url": "https://api.github.com/repos/composer/semver/zipball/38276325bd896f90dfcfe30029aa5db40df387a7", + "reference": "38276325bd896f90dfcfe30029aa5db40df387a7", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5", - "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + "phpunit/phpunit": "^4.5 || ^5.0.5" }, "type": "library", "extra": { @@ -337,7 +373,137 @@ "validation", "versioning" ], - "time": "2016-08-30T16:08:34+00:00" + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/1.7.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-09-27T13:13:07+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "1.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6e076a124f7ee146f2487554a94b6a19a74887ba", + "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/1.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:39:10+00:00" + }, + { + "name": "cweagans/composer-patches", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "ae02121445ad75f4eaff800cc532b5e6233e2ddf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/ae02121445ad75f4eaff800cc532b5e6233e2ddf", + "reference": "ae02121445ad75f4eaff800cc532b5e6233e2ddf", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "composer/composer": "~1.0 || ~2.0", + "phpunit/phpunit": "~4.6" + }, + "type": "composer-plugin", + "extra": { + "class": "cweagans\\Composer\\Patches" + }, + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a way to patch Composer packages.", + "support": { + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/1.7.0" + }, + "time": "2020-09-30T17:56:20+00:00" }, { "name": "doctrine/annotations", @@ -405,6 +571,9 @@ "docblock", "parser" ], + "support": { + "source": "https://github.com/doctrine/annotations/tree/master" + }, "time": "2015-08-31T12:32:49+00:00" }, { @@ -459,25 +628,32 @@ "constructor", "instantiate" ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/master" + }, "time": "2015-06-14T21:17:01+00:00" }, { "name": "doctrine/lexer", - "version": "v1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" + "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/1febd6c3ef84253d7c815bed85fc622ad207a9f8", + "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8", "shasum": "" }, "require": { "php": ">=5.3.2" }, + "require-dev": { + "phpunit/phpunit": "^4.5" + }, "type": "library", "extra": { "branch-alias": { @@ -485,8 +661,8 @@ } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Lexer\\": "lib/" + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" } }, "notification-url": "https://packagist.org/downloads/", @@ -507,34 +683,41 @@ "email": "schmittjoh@gmail.com" } ], - "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "http://www.doctrine-project.org", + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", "keywords": [ + "annotations", + "docblock", "lexer", - "parser" + "parser", + "php" ], - "time": "2014-09-09T13:34:57+00:00" + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.0.2" + }, + "time": "2019-06-08T11:03:04+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.2.12", + "version": "v2.2.20", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "362ad63e80d05a238fbe542d75cc1f8e9246797e" + "reference": "f1631f0747ad2a9dd3de8d7873b71f6573f8d0c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/362ad63e80d05a238fbe542d75cc1f8e9246797e", - "reference": "362ad63e80d05a238fbe542d75cc1f8e9246797e", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/f1631f0747ad2a9dd3de8d7873b71f6573f8d0c2", + "reference": "f1631f0747ad2a9dd3de8d7873b71f6573f8d0c2", "shasum": "" }, "require": { "composer/semver": "^1.4", + "composer/xdebug-handler": "^1.0", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", - "gecko-packages/gecko-php-unit": "^2.0", "php": "^5.3.6 || >=7.0 <7.3", "sebastian/diff": "^1.4", "symfony/console": "^2.4 || ^3.0 || ^4.0", @@ -553,34 +736,37 @@ "hhvm": "<3.18" }, "require-dev": { - "johnkary/phpunit-speedtrap": "^1.0.1", + "johnkary/phpunit-speedtrap": "^1.0.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", + "keradus/cli-executor": "^1.1", + "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^1.0.2", "phpunit/phpunit": "^4.8.35 || ^5.4.3", "symfony/phpunit-bridge": "^3.2.2 || ^4.0" }, "suggest": { "ext-mbstring": "For handling non-UTF8 characters in cache signature.", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." }, "bin": [ "php-cs-fixer" ], "type": "application", - "extra": { - "branch-alias": { - "dev-master": "2.2-dev" - } - }, "autoload": { "psr-4": { "PhpCsFixer\\": "src/" }, "classmap": [ "tests/Test/AbstractFixerTestCase.php", + "tests/Test/AbstractIntegrationCaseFactory.php", "tests/Test/AbstractIntegrationTestCase.php", "tests/Test/IntegrationCase.php", - "tests/Test/IntegrationCaseFactory.php" + "tests/Test/IntegrationCaseFactory.php", + "tests/Test/IntegrationCaseFactoryInterface.php", + "tests/Test/InternalIntegrationCaseFactory.php", + "tests/TestCase.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -598,51 +784,11 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2017-11-26T20:41:43+00:00" - }, - { - "name": "gecko-packages/gecko-php-unit", - "version": "v2.2", - "source": { - "type": "git", - "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git", - "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/ab525fac9a9ffea219687f261b02008b18ebf2d1", - "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1", - "shasum": "" + "support": { + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/2.2" }, - "require": { - "php": "^5.3.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.4.3" - }, - "suggest": { - "ext-dom": "When testing with xml.", - "ext-libxml": "When testing with xml.", - "phpunit/phpunit": "This is an extension for it so make sure you have it some way." - }, - "type": "library", - "autoload": { - "psr-4": { - "GeckoPackages\\PHPUnit\\": "src/PHPUnit" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Additional PHPUnit asserts and constraints.", - "homepage": "https://github.com/GeckoPackages", - "keywords": [ - "extension", - "filesystem", - "phpunit" - ], - "time": "2017-08-23T07:39:54+00:00" + "time": "2018-06-02T17:26:04+00:00" }, { "name": "ircmaxell/password-compat", @@ -684,6 +830,10 @@ "hashing", "password" ], + "support": { + "issues": "https://github.com/ircmaxell/password_compat/issues", + "source": "https://github.com/ircmaxell/password_compat/tree/v1.0" + }, "time": "2014-11-20T16:49:30+00:00" }, { @@ -734,20 +884,24 @@ "profile", "slow" ], + "support": { + "issues": "https://github.com/johnkary/phpunit-speedtrap/issues", + "source": "https://github.com/johnkary/phpunit-speedtrap/tree/master" + }, "time": "2015-09-13T19:01:00+00:00" }, { "name": "paragonie/random_compat", - "version": "v2.0.11", + "version": "v2.0.19", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8" + "reference": "446fc9faa5c2a9ddf65eb7121c0af7e857295241" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8", - "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/446fc9faa5c2a9ddf65eb7121c0af7e857295241", + "reference": "446fc9faa5c2a9ddf65eb7121c0af7e857295241", "shasum": "" }, "require": { @@ -779,10 +933,16 @@ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", "keywords": [ "csprng", + "polyfill", "pseudorandom", "random" ], - "time": "2017-09-27T21:40:39+00:00" + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T10:06:57+00:00" }, { "name": "phpdocumentor/reflection-docblock", @@ -831,42 +991,46 @@ "email": "mike.vanriel@naenius.com" } ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/2.x" + }, "time": "2016-01-25T08:17:30+00:00" }, { "name": "phpspec/prophecy", - "version": "1.7.3", + "version": "v1.10.3", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf" + "reference": "451c3cd1418cf640de218914901e51b064abb093" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", - "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", + "reference": "451c3cd1418cf640de218914901e51b064abb093", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" }, "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7" + "phpspec/phpspec": "^2.5 || ^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.10.x-dev" } }, "autoload": { - "psr-0": { - "Prophecy\\": "src/" + "psr-4": { + "Prophecy\\": "src/Prophecy" } }, "notification-url": "https://packagist.org/downloads/", @@ -894,7 +1058,11 @@ "spy", "stub" ], - "time": "2017-11-24T13:59:53+00:00" + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" + }, + "time": "2020-03-05T15:02:03+00:00" }, { "name": "phpunit/php-code-coverage", @@ -956,6 +1124,11 @@ "testing", "xunit" ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/2.2" + }, "time": "2015-10-06T15:47:00+00:00" }, { @@ -1003,6 +1176,11 @@ "filesystem", "iterator" ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5" + }, "time": "2017-11-27T13:52:08+00:00" }, { @@ -1044,6 +1222,10 @@ "keywords": [ "template" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, "time": "2015-06-21T13:50:34+00:00" }, { @@ -1093,20 +1275,24 @@ "keywords": [ "timer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/master" + }, "time": "2017-02-26T11:10:40+00:00" }, { "name": "phpunit/php-token-stream", - "version": "1.4.11", + "version": "1.4.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" + "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16", + "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16", "shasum": "" }, "require": { @@ -1142,7 +1328,12 @@ "keywords": [ "tokenizer" ], - "time": "2017-02-27T10:12:30+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/1.4" + }, + "abandoned": true, + "time": "2017-12-04T08:55:13+00:00" }, { "name": "phpunit/phpunit", @@ -1214,6 +1405,10 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/4.8.34" + }, "time": "2017-01-26T16:15:36+00:00" }, { @@ -1270,20 +1465,26 @@ "mock", "xunit" ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues", + "source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/2.3" + }, + "abandoned": true, "time": "2015-10-02T06:51:40+00:00" }, { "name": "psr/log", - "version": "1.0.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", "shasum": "" }, "require": { @@ -1292,7 +1493,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -1317,7 +1518,10 @@ "psr", "psr-3" ], - "time": "2016-10-10T12:19:37+00:00" + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" + }, + "time": "2020-03-23T09:12:05+00:00" }, { "name": "sebastian/comparator", @@ -1381,6 +1585,10 @@ "compare", "equality" ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/1.2" + }, "time": "2017-01-29T09:50:25+00:00" }, { @@ -1433,6 +1641,10 @@ "keywords": [ "diff" ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/1.4" + }, "time": "2017-05-22T07:24:03+00:00" }, { @@ -1483,6 +1695,10 @@ "environment", "hhvm" ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/1.3" + }, "time": "2016-08-18T05:49:44+00:00" }, { @@ -1550,6 +1766,10 @@ "export", "exporter" ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/master" + }, "time": "2016-06-17T09:04:28+00:00" }, { @@ -1601,6 +1821,10 @@ "keywords": [ "global state" ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/1.1.1" + }, "time": "2015-10-12T03:26:01+00:00" }, { @@ -1654,6 +1878,10 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/master" + }, "time": "2016-10-03T07:41:43+00:00" }, { @@ -1689,20 +1917,24 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/1.0.6" + }, "time": "2015-06-21T13:59:46+00:00" }, { "name": "symfony/console", - "version": "v2.8.31", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7cad097cf081c0ab3d0322cc38d34ee80484d86f" + "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7cad097cf081c0ab3d0322cc38d34ee80484d86f", - "reference": "7cad097cf081c0ab3d0322cc38d34ee80484d86f", + "url": "https://api.github.com/repos/symfony/console/zipball/cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12", + "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12", "shasum": "" }, "require": { @@ -1716,7 +1948,7 @@ "symfony/process": "~2.1|~3.0.0" }, "suggest": { - "psr/log": "For using the console logger", + "psr/log-implementation": "For using the console logger", "symfony/event-dispatcher": "", "symfony/process": "" }, @@ -1750,20 +1982,23 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-11-16T15:20:19+00:00" + "support": { + "source": "https://github.com/symfony/console/tree/v2.8.52" + }, + "time": "2018-11-20T15:55:20+00:00" }, { "name": "symfony/debug", - "version": "v2.8.31", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "a0a29e9867debabdace779a20a9385c623a23bbd" + "reference": "74251c8d50dd3be7c4ce0c7b862497cdc641a5d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/a0a29e9867debabdace779a20a9385c623a23bbd", - "reference": "a0a29e9867debabdace779a20a9385c623a23bbd", + "url": "https://api.github.com/repos/symfony/debug/zipball/74251c8d50dd3be7c4ce0c7b862497cdc641a5d0", + "reference": "74251c8d50dd3be7c4ce0c7b862497cdc641a5d0", "shasum": "" }, "require": { @@ -1807,20 +2042,23 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-10-24T13:48:52+00:00" + "support": { + "source": "https://github.com/symfony/debug/tree/v2.8.50" + }, + "time": "2018-11-11T11:18:13+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.31", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b59aacf238fadda50d612c9de73b74751872a903" + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b59aacf238fadda50d612c9de73b74751872a903", - "reference": "b59aacf238fadda50d612c9de73b74751872a903", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a77e974a5fecb4398833b0709210e3d5e334ffb0", + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0", "shasum": "" }, "require": { @@ -1867,24 +2105,28 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-11-05T15:25:56+00:00" + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v2.8.50" + }, + "time": "2018-11-21T14:20:20+00:00" }, { "name": "symfony/filesystem", - "version": "v2.8.31", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "10507c5f24577b0ad971b0d22097c823b2b45dd3" + "reference": "7ae46872dad09dffb7fe1e93a0937097339d0080" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/10507c5f24577b0ad971b0d22097c823b2b45dd3", - "reference": "10507c5f24577b0ad971b0d22097c823b2b45dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7ae46872dad09dffb7fe1e93a0937097339d0080", + "reference": "7ae46872dad09dffb7fe1e93a0937097339d0080", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { @@ -1916,20 +2158,23 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-11-07T14:08:47+00:00" + "support": { + "source": "https://github.com/symfony/filesystem/tree/v2.8.52" + }, + "time": "2018-11-11T11:18:13+00:00" }, { "name": "symfony/finder", - "version": "v2.8.31", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "efeceae6a05a9b2fcb3391333f1d4a828ff44ab8" + "reference": "1444eac52273e345d9b95129bf914639305a9ba4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/efeceae6a05a9b2fcb3391333f1d4a828ff44ab8", - "reference": "efeceae6a05a9b2fcb3391333f1d4a828ff44ab8", + "url": "https://api.github.com/repos/symfony/finder/zipball/1444eac52273e345d9b95129bf914639305a9ba4", + "reference": "1444eac52273e345d9b95129bf914639305a9ba4", "shasum": "" }, "require": { @@ -1965,20 +2210,23 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-11-05T15:25:56+00:00" + "support": { + "source": "https://github.com/symfony/finder/tree/v2.8.50" + }, + "time": "2018-11-11T11:18:13+00:00" }, { "name": "symfony/options-resolver", - "version": "v2.8.31", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "e4e64cb8e01981425098bfb6000ac397206dfc25" + "reference": "7aaab725bb58f0e18aa12c61bdadd4793ab4c32b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/e4e64cb8e01981425098bfb6000ac397206dfc25", - "reference": "e4e64cb8e01981425098bfb6000ac397206dfc25", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7aaab725bb58f0e18aa12c61bdadd4793ab4c32b", + "reference": "7aaab725bb58f0e18aa12c61bdadd4793ab4c32b", "shasum": "" }, "require": { @@ -2019,20 +2267,102 @@ "configuration", "options" ], - "time": "2017-11-05T15:25:56+00:00" + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v2.8.50" + }, + "time": "2018-11-11T11:18:13+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.19.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/aed596913b70fae57be53d86faa2e9ef85a2297b", + "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.19-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.19.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.6.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296" + "reference": "b5f7b932ee6fa802fc792eabd77c4c88084517ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", - "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b5f7b932ee6fa802fc792eabd77c4c88084517ce", + "reference": "b5f7b932ee6fa802fc792eabd77c4c88084517ce", "shasum": "" }, "require": { @@ -2044,7 +2374,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-main": "1.19-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -2078,20 +2412,37 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.19.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-php54", - "version": "v1.6.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php54.git", - "reference": "d7810a14b2c6c1aff415e1bb755f611b3d5327bc" + "reference": "c248bab30dad46a5f3917e7d92907e148bdc50c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/d7810a14b2c6c1aff415e1bb755f611b3d5327bc", - "reference": "d7810a14b2c6c1aff415e1bb755f611b3d5327bc", + "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/c248bab30dad46a5f3917e7d92907e148bdc50c6", + "reference": "c248bab30dad46a5f3917e7d92907e148bdc50c6", "shasum": "" }, "require": { @@ -2100,7 +2451,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-main": "1.19-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -2136,20 +2491,37 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php54/tree/v1.19.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-php55", - "version": "v1.6.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php55.git", - "reference": "b64e7f0c37ecf144ecc16668936eef94e628fbfd" + "reference": "248a5c9877b126493abb661e4fb47792e418035b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/b64e7f0c37ecf144ecc16668936eef94e628fbfd", - "reference": "b64e7f0c37ecf144ecc16668936eef94e628fbfd", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/248a5c9877b126493abb661e4fb47792e418035b", + "reference": "248a5c9877b126493abb661e4fb47792e418035b", "shasum": "" }, "require": { @@ -2159,7 +2531,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-main": "1.19-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -2192,30 +2568,51 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php55/tree/v1.19.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.6.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff" + "reference": "3fe414077251a81a1b15b1c709faf5c2fbae3d4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff", - "reference": "0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/3fe414077251a81a1b15b1c709faf5c2fbae3d4e", + "reference": "3fe414077251a81a1b15b1c709faf5c2fbae3d4e", "shasum": "" }, "require": { - "paragonie/random_compat": "~1.0|~2.0", + "paragonie/random_compat": "~1.0|~2.0|~9.99", "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-main": "1.19-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -2251,20 +2648,37 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php70/tree/v1.19.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.6.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "6de4f4884b97abbbed9f0a84a95ff2ff77254254" + "reference": "beecef6b463b06954638f02378f52496cb84bacc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/6de4f4884b97abbbed9f0a84a95ff2ff77254254", - "reference": "6de4f4884b97abbbed9f0a84a95ff2ff77254254", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/beecef6b463b06954638f02378f52496cb84bacc", + "reference": "beecef6b463b06954638f02378f52496cb84bacc", "shasum": "" }, "require": { @@ -2273,7 +2687,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-main": "1.19-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -2306,20 +2724,37 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.19.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/process", - "version": "v2.8.31", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d25449e031f600807949aab7cadbf267712f4eee" + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d25449e031f600807949aab7cadbf267712f4eee", - "reference": "d25449e031f600807949aab7cadbf267712f4eee", + "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8", + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8", "shasum": "" }, "require": { @@ -2355,20 +2790,23 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-11-05T15:25:56+00:00" + "support": { + "source": "https://github.com/symfony/process/tree/v2.8.50" + }, + "time": "2018-11-11T11:18:13+00:00" }, { "name": "symfony/stopwatch", - "version": "v2.8.31", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "533bb9d7c2da1c6d2da163ecf0f22043ea98f59b" + "reference": "752586c80af8a85aeb74d1ae8202411c68836663" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/533bb9d7c2da1c6d2da163ecf0f22043ea98f59b", - "reference": "533bb9d7c2da1c6d2da163ecf0f22043ea98f59b", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/752586c80af8a85aeb74d1ae8202411c68836663", + "reference": "752586c80af8a85aeb74d1ae8202411c68836663", "shasum": "" }, "require": { @@ -2404,24 +2842,28 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2017-11-10T18:59:36+00:00" + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v2.8.52" + }, + "time": "2018-11-11T11:18:13+00:00" }, { "name": "symfony/yaml", - "version": "v2.8.31", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d819bf267e901727141fe828ae888486fd21236e" + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d819bf267e901727141fe828ae888486fd21236e", - "reference": "d819bf267e901727141fe828ae888486fd21236e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/02c1859112aa779d9ab394ae4f3381911d84052b", + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { @@ -2453,7 +2895,10 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-11-05T15:25:56+00:00" + "support": { + "source": "https://github.com/symfony/yaml/tree/v2.8.52" + }, + "time": "2018-11-11T11:18:13+00:00" } ], "aliases": [], @@ -2472,5 +2917,6 @@ "platform-dev": [], "platform-overrides": { "php": "5.4" - } + }, + "plugin-api-version": "2.0.0" } diff --git a/docs/guide-fr/concept-di-container.md b/docs/guide-fr/concept-di-container.md index 883699d..6cba3d2 100644 --- a/docs/guide-fr/concept-di-container.md +++ b/docs/guide-fr/concept-di-container.md @@ -410,7 +410,7 @@ $container->setDefinitions([ ] ]); -$reader = $container->get('app\storage\DocumentsReader); +$reader = $container->get('app\storage\DocumentsReader'); // Se comporte exactement comme l'exemple précédent ``` diff --git a/docs/guide-fr/runtime-requests.md b/docs/guide-fr/runtime-requests.md index 03315cd..e2bcdc7 100644 --- a/docs/guide-fr/runtime-requests.md +++ b/docs/guide-fr/runtime-requests.md @@ -93,7 +93,7 @@ et avant le nom du script d'entrée. -## Enntêtes HTTP +## Entêtes HTTP Vous pouvez obtenir les entêtes HTTP via la [[yii\web\HeaderCollection|collection d'entêtes]] qui est retournée par la propriété [[yii\web\Request::headers]]. Par exemple : diff --git a/docs/guide-fr/start-installation.md b/docs/guide-fr/start-installation.md index 6694bcd..6557961 100644 --- a/docs/guide-fr/start-installation.md +++ b/docs/guide-fr/start-installation.md @@ -15,14 +15,14 @@ Dans cette section et quelques-unes de ses suivantes, nous décrirons comment in Installer via Composer ---------------------- -###Installer Composer +### Installer Composer Si vous n'avez pas déjà installé Composer, vous pouvez le faire en suivant les instructions du site [getcomposer.org](https://getcomposer.org/download/). Sous Linux et Mac OS X, vous pouvez exécuter les commandes : ```bash - curl -sS https://getcomposer.org/installer | php - mv composer.phar /usr/local/bin/composer +curl -sS https://getcomposer.org/installer | php +mv composer.phar /usr/local/bin/composer ``` Sous Windows, téléchargez et exécutez [Composer-Setup.exe](https://getcomposer.org/Composer-Setup.exe). @@ -39,12 +39,12 @@ Si Composer était déjà installé auparavant, assurez-vous d'utiliser une vers > Reportez-vous à la [documentation de Composer sur les jetons de l'API Github](https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens) > pour savoir comment procéder. -###Installer Yii - +### Installer Yii Avec Composer installé, vous pouvez installer le modèle de projet Yii en exécutant la commande suivante dans un dossier accessible via le Web : + ```bash - composer create-project --prefer-dist yiisoft/yii2-app-basic basic +composer create-project --prefer-dist yiisoft/yii2-app-basic basic ``` Cette commande installera la dernière version stable du modèle de projet Yii dans le dossier `basic`. Vous êtes libre de choisir un autre dossier si vous le désirez. @@ -53,11 +53,10 @@ Cette commande installera la dernière version stable du modèle de projet Yii d > [Troubleshooting (résolution des problèmes) de la documentation de Composer](https://getcomposer.org/doc/articles/troubleshooting.md) > pour les erreurs communes. Une fois l'erreur corrigée, vous pouvez reprendre l'installation avortée en exécutant `composer update` dans le dossier `basic` (ou celui que vous aviez choisi). - > Tip: si vous souhaitez installer la dernière version de développement de Yii, vous pouvez utiliser la commande suivante qui ajoutera l'[option stability](https://getcomposer.org/doc/04-schema.md#minimum-stability) : > >```bash -> composer create-project --prefer-dist --stability=dev yiisoft/yii2-app-basic basic +>composer create-project --prefer-dist --stability=dev yiisoft/yii2-app-basic basic >``` > > Notez que la version de développement de Yii ne doit pas être utilisée en production, vu qu'elle pourrait *casser* votre code existant. @@ -73,9 +72,9 @@ Installer Yii depuis une archive se fait en trois étapes : 3. Modifier le fichier `config/web.php` en entrant une clé secrète pour la configuration de `cookieValidationKey` (cela est fait automatiquement si vous installez Yii avec Composer) : ```php - // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation - 'cookieValidationKey' => 'enter your secret key here', - ``` + // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation + 'cookieValidationKey' => 'enter your secret key here', + ``` Autres options d'installation @@ -116,7 +115,7 @@ Afin d'empêcher l'installation des « assets » via Composer, ajoutez les lig Vérifier l'installation ----------------------- -Après l'installation, vous pouvez, soit configurer votre serveur Web (voir section suivante), soit utiliser le [serveur PHP web incorporé](https://secure.php.net/manual/fr/features.commandline.webserver.php) en utilisant la commande en console suivante depuis le dossier `web` de votre projet : +Après l'installation, vous pouvez, soit configurer votre serveur Web (voir section suivante), soit utiliser le [serveur PHP web incorporé](https://secure.php.net/manual/fr/features.commandline.webserver.php) en utilisant la commande en console suivante depuis le dossier racine de votre projet : ```bash php yii serve diff --git a/docs/guide-fr/structure-filters.md b/docs/guide-fr/structure-filters.md index e198d35..90e4488 100644 --- a/docs/guide-fr/structure-filters.md +++ b/docs/guide-fr/structure-filters.md @@ -88,6 +88,7 @@ Yii fournit un jeu de filtres couramment utilisés, que l'on trouve en premier l *AccessControl* (contrôle d'accès) fournit un contrôle d'accès simple basé sur un jeu de [[yii\filters\AccessControl::rules|règles]]. En particulier, avant qu'une action ne soit exécutée, *AccessControl* examine les règles listées et trouve la première qui correspond aux variables du contexte courant (comme l'adresse IP, l'état de connexion de l'utilisateur, etc.). La règle qui correspond détermine si l'exécution de l'action requise doit être autorisée ou refusée. Si aucune des règles ne correspond, l'accès est refusé. L'exemple suivant montre comment autoriser les utilisateurs authentifiés à accéder aux actions `create` et `update` tout en refusant l'accès à ces actions aux autres utilisateurs. + ```php use yii\filters\AccessControl; @@ -141,6 +142,7 @@ Les filtres de méthode d'authentification sont communément utilisés dans la m *ContentNegotiator* (négociateur de contenu) prend en charge la négociation des formats de réponse et la négociation de langue d'application. Il essaye de déterminer le format de la réponse et/ou la langue en examinant les paramètres de la méthode `GET` et ceux de l'entête HTTP `Accept`. Dans l'exemple qui suit, le filtre *ContentNegotiator* est configuré pour prendre en charge JSON et XML en tant que formats de réponse, et anglais (États-Unis) et allemand en tant que langues. + ```php use yii\filters\ContentNegotiator; use yii\web\Response; diff --git a/docs/guide-ja/concept-di-container.md b/docs/guide-ja/concept-di-container.md index 4c47c95..8daac4f 100644 --- a/docs/guide-ja/concept-di-container.md +++ b/docs/guide-ja/concept-di-container.md @@ -169,6 +169,9 @@ $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer'); // Connection のインスタンスを作成できます $container->set('foo', 'yii\db\Connection'); +// `Instance::of` を使ってエイリアスの登録。 +$container->set('bar', Instance::of('foo')); + // 構成情報をともなうクラスの登録。クラスが get() でインスタンス化 // されるとき構成情報が適用されます $container->set('yii\db\Connection', [ @@ -179,7 +182,7 @@ $container->set('yii\db\Connection', [ ]); // クラスの構成情報をともなうエイリアス名の登録 -// この場合、クラスを指定する "class" 要素が必要です +// この場合、クラスを指定する "class" または "__class" 要素が必要です $container->set('db', [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', @@ -188,11 +191,12 @@ $container->set('db', [ 'charset' => 'utf8', ]); -// PHP コーラブルの登録 +// コーラブルなクロージャまたは配列の登録 // このコーラブルは $container->get('db') が呼ばれるたびに実行されます $container->set('db', function ($container, $params, $config) { return new \yii\db\Connection($config); }); +$container->set('db', ['app\db\DbFactory', 'create']); // コンポーネント・インスタンスの登録 // $container->get('pageCache') は呼ばれるたびに毎回同じインスタンスを返します @@ -215,7 +219,6 @@ $container->setSingleton('yii\db\Connection', [ ]); ``` - 依存を解決する -------------- @@ -372,6 +375,24 @@ class HotelController extends Controller これで、あなたが再びコントローラにアクセスするときは、`app\components\BookingService` のインスタンスが作成され、コントローラのコンストラクタに3番目のパラメータとして注入されるようになります。 +Yii 2.0.36 以降は、PHP 7 を使う場合に、ウェブおよびコンソール両方のコントローラでアクション・インジェクションを利用することが出来ます。 + +```php +namespace app\controllers; + +use yii\web\Controller; +use app\components\BookingInterface; + +class HotelController extends Controller +{ + public function actionBook($id, BookingInterface $bookingService) + { + $result = $bookingService->book($id); + // ... + } +} +``` + 高度な実際の使用方法 -------------------- @@ -427,7 +448,7 @@ $container->setDefinitions([ } ]); -$reader = $container->get('app\storage\DocumentsReader); +$reader = $container->get('app\storage\DocumentsReader'); // 構成情報に書かれている依存とともに DocumentReader オブジェクトが生成されます ``` @@ -440,32 +461,27 @@ $reader = $container->get('app\storage\DocumentsReader); [依存を解決する](#resolving-dependencies) のセクションで説明したように、[[yii\di\Container::set()|set()]] と [[yii\di\Container::setSingleton()|setSingleton()]] は、 オプションで、第三の引数として依存のコンストラクタのパラメータを取ることが出来ます。 -コンストラクタのパラメータを設定するために、以下の構成情報配列の形式を使うことが出来ます。 - - - `key`: クラス名、インタフェイス名、または、エイリアス名。 - このキーが [[yii\di\Container::set()|set()]] メソッドの最初の引数 `$class` として渡されます。 - - `value`: 二つの要素を持つ配列。最初の要素は [[set()]] メソッドに二番目のパラメータ `$definition` - として渡され、第二の要素が `$params` として渡されます。 +コンストラクタのパラメータを設定するために、`__construct()` オプションを使うことが出来ます。 では、私たちの例を修正しましょう。 ```php $container->setDefinitions([ 'tempFileStorage' => [ // 便利なようにエイリアスを作りました - ['class' => 'app\storage\FileStorage'], - ['/var/tempfiles'] // 何らかの構成ファイルから抽出することも可能 + 'class' => 'app\storage\FileStorage', + '__construct()' => ['/var/tempfiles'], // 何らかの構成ファイルから抽出することも可能 ], 'app\storage\DocumentsReader' => [ - ['class' => 'app\storage\DocumentsReader'], - [Instance::of('tempFileStorage')] + 'class' => 'app\storage\DocumentsReader', + '__construct()' => [Instance::of('tempFileStorage')], ], 'app\storage\DocumentsWriter' => [ - ['class' => 'app\storage\DocumentsWriter'], - [Instance::of('tempFileStorage')] + 'class' => 'app\storage\DocumentsWriter', + '__construct()' => [Instance::of('tempFileStorage')] ] ]); -$reader = $container->get('app\storage\DocumentsReader); +$reader = $container->get('app\storage\DocumentsReader'); // 前の例と全く同じオブジェクトが生成されます ``` @@ -488,19 +504,19 @@ $reader = $container->get('app\storage\DocumentsReader); ```php $container->setSingletons([ 'tempFileStorage' => [ - ['class' => 'app\storage\FileStorage'], - ['/var/tempfiles'] + 'class' => 'app\storage\FileStorage', + '__construct()' => ['/var/tempfiles'] ], ]); $container->setDefinitions([ 'app\storage\DocumentsReader' => [ - ['class' => 'app\storage\DocumentsReader'], - [Instance::of('tempFileStorage')] + 'class' => 'app\storage\DocumentsReader', + '__construct()' => [Instance::of('tempFileStorage')], ], 'app\storage\DocumentsWriter' => [ - ['class' => 'app\storage\DocumentsWriter'], - [Instance::of('tempFileStorage')] + 'class' => 'app\storage\DocumentsWriter', + '__construct()' => [Instance::of('tempFileStorage')], ] ]); diff --git a/docs/guide-ja/db-dao.md b/docs/guide-ja/db-dao.md index f8f4da7..f55a6e3 100644 --- a/docs/guide-ja/db-dao.md +++ b/docs/guide-ja/db-dao.md @@ -19,12 +19,6 @@ Yii 2.0 では、DAO は下記の DBMS のサポートを内蔵しています - [Oracle](http://www.oracle.com/us/products/database/overview/index.html) - [MSSQL](https://www.microsoft.com/en-us/sqlserver/default.aspx): バージョン 2008 以上。 -> Info: Yii 2.1 以降では、CUBRID、Oracle および MSSQL に対する DAO サポートは、フレームワーク内蔵のコア・コンポーネント - としては提供されていません。それらは、独立した [エクステンション](structure-extensions.md) としてインストールされる - 必要があります。[yiisoft/yii2-oracle](https://www.yiiframework.com/extension/yiisoft/yii2-oracle) および - [yiisoft/yii2-mssql](https://www.yiiframework.com/extension/yiisoft/yii2-mssql) が - [公式エクステンション](https://www.yiiframework.com/extensions/official) として提供されています。 - > Note: PHP 7 用の pdo_oci の新しいバージョンは、現在、ソース・コードとしてのみ存在します。 [コミュニティによる説明](https://github.com/yiisoft/yii2/issues/10975#issuecomment-248479268) に従ってコンパイルするか、 または、[PDO エミュレーション・レイヤ](https://github.com/taq/pdooci) を使って下さい。 @@ -113,6 +107,18 @@ ODBC 経由でデータベースに接続しようとする場合は、[[yii\db\ > ] > ``` +MS SQL Server でバイナリ・データを正しく処理するためには追加の接続オプションが必要になります。 + +```php +'db' => [ + 'class' => 'yii\db\Connection', + 'dsn' => 'sqlsrv:Server=localhost;Database=mydatabase', + 'attributes' => [ + \PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_SYSTEM + ] +], +``` + ## SQL クエリを実行する diff --git a/docs/guide-ja/db-query-builder.md b/docs/guide-ja/db-query-builder.md index f24a7d0..02221ea 100644 --- a/docs/guide-ja/db-query-builder.md +++ b/docs/guide-ja/db-query-builder.md @@ -230,7 +230,7 @@ $query->where(['id' => $userQuery]); ハッシュ形式を使う場合、Yii は内部的にパラメータ・バインディングを使用します。 従って、[文字列形式](#string-format) とは対照的に、ここでは手動でパラメータを追加する必要はありません。ただし、Yii はカラム名を決してエスケープしないことに注意して下さい。 従って、ユーザから取得した変数を何ら追加のチェックをすることなくカラム名として渡すと、SQL インジェクション攻撃に対して脆弱になります。 -アプリケーションを安全に保つためには、カラム名として変数を使わないこと、または、変数をホワイト・リストによってフィルターすることが必要です。 +アプリケーションを安全に保つためには、カラム名として変数を使わないこと、または、変数を許容リストによってフィルターすることが必要です。 カラム名をユーザから取得する必要がある場合は、ガイドの [データをフィルタリングする](output-data-widgets.md#filtering-data) という記事を読んで下さい。 例えば、次のコードは脆弱です。 @@ -321,7 +321,7 @@ $query->where([$column => $value]); 演算子形式を使う場合、Yii は値に対して内部的にパラメータ・バインディングを使用します。 従って、[文字列形式](#string-format) とは対照的に、ここでは手動でパラメータを追加する必要はありません。 ただし、Yii はカラム名を決してエスケープしないことに注意して下さい。従って、変数をカラム名として渡すと、アプリケーションは SQL インジェクション攻撃に対して脆弱になります。 -アプリケーションを安全に保つためには、カラム名として変数を使わないこと、または、変数をホワイト・リストによってフィルターすることが必要です。 +アプリケーションを安全に保つためには、カラム名として変数を使わないこと、または、変数を許容リストによってフィルターすることが必要です。 カラム名をユーザから取得する必要がある場合は、ガイドの [データをフィルタリングする](output-data-widgets.md#filtering-data) という記事を読んで下さい。 例えば、次のコードは脆弱です。 @@ -602,6 +602,28 @@ $query1->union($query2); [[yii\db\Query::union()|union()]] を複数回呼んで、`UNION` 句をさらに追加することが出来ます。 +### [[yii\db\Query::withQuery()|withQuery()]] + +[[yii\db\Query::withQuery()|withQuery()]] メソッドは SQL クエリの `WITH` プレフィックスを指定するものです。サブクエリの代りに `WITH` を使うと読みやすさを向上させ、ユニークな機能(再帰 CTE)を利用することが出来ます。詳細は [modern-sql](https://modern-sql.com/feature/with) を参照して下さい。例えば、次のクエリは `admin` の持つ権限をその子も含めて全て再帰的に取得します。 + +```php +$initialQuery = (new \yii\db\Query()) + ->select(['parent', 'child']) + ->from(['aic' => 'auth_item_child']) + ->where(['parent' => 'admin']); + +$recursiveQuery = (new \yii\db\Query()) + ->select(['aic.parent', 'aic.child']) + ->from(['aic' => 'auth_item_child']) + ->innerJoin('t1', 't1.child = aic.parent'); + +$mainQuery = (new \yii\db\Query()) + ->select(['parent', 'child']) + ->from('t1') + ->withQuery($initialQuery->union($recursiveQuery), 't1', true); +``` + +[[yii\db\Query::withQuery()|withQuery()]] を複数回呼び出してさらなる CTE をメイン・クエリに追加することが出来ます。クエリはアタッチされたのと同じ順序でプリペンドされます。クエリのうちの一つが再帰的である場合は CTE 全体が再帰的になります。 ## クエリ・メソッド diff --git a/docs/guide-ja/helper-html.md b/docs/guide-ja/helper-html.md index 4df8c5a..14693f2 100644 --- a/docs/guide-ja/helper-html.md +++ b/docs/guide-ja/helper-html.md @@ -327,18 +327,18 @@ echo Html::getAttributeName('dates[0]'); 埋め込みのスタイルとスクリプトをラップするタグを生成するメソッドが二つあります。 ```php - + 'print']) ?> これは次の HTML を生成します。 - + - true]) ?> + これは次の HTML を生成します。 - + ``` CSS ファイルの外部スタイルをリンクしたい場合は、次のようにします。 diff --git a/docs/guide-ja/input-validation.md b/docs/guide-ja/input-validation.md index 9699df5..6850752 100644 --- a/docs/guide-ja/input-validation.md +++ b/docs/guide-ja/input-validation.md @@ -355,8 +355,10 @@ Yii のリリースに含まれている [コア・バリデータ](tutorial-cor * @param mixed $params 規則に与えられる "params" の値 * @param \yii\validators\InlineValidator $validator 関係する InlineValidator のインスタンス。 * このパラメータは、バージョン 2.0.11 以降で利用可能。 + * @param mixed $current 現在検証されている属性の値 + * このパラメータは、バージョン 2.0.36 以降で利用可能。 */ -function ($attribute, $params, $validator) +function ($attribute, $params, $validator, $current) ``` 属性が検証に失敗した場合は、メソッド/関数 は [[yii\base\Model::addError()]] を呼んでエラー・メッセージをモデルに保存し、 diff --git a/docs/guide-ja/output-data-widgets.md b/docs/guide-ja/output-data-widgets.md index a87ceed..712a5d6 100644 --- a/docs/guide-ja/output-data-widgets.md +++ b/docs/guide-ja/output-data-widgets.md @@ -335,6 +335,7 @@ GridView に CheckboxColumn を追加するためには、以下のようにし ```php echo GridView::widget([ + 'id' => 'grid', 'dataProvider' => $dataProvider, 'columns' => [ // ... diff --git a/docs/guide-ja/rest-quick-start.md b/docs/guide-ja/rest-quick-start.md index 4243daf..79e4c61 100644 --- a/docs/guide-ja/rest-quick-start.md +++ b/docs/guide-ja/rest-quick-start.md @@ -162,7 +162,9 @@ Content-Type: application/xml 次のコマンドは、JSON 形式でユーザのデータを持つ POST リクエストを送信して、新しいユーザを作成します。 ``` -$ curl -i -H "Accept:application/json" -H "Content-Type:application/json" -XPOST "http://localhost/users" -d '{"username": "example", "email": "user@example.com"}' +$ curl -i -H "Accept:application/json" -H "Content-Type:application/json" \ + -XPOST "http://localhost/users" \ + -d '{"username": "example", "email": "user@example.com"}' HTTP/1.1 201 Created ... diff --git a/docs/guide-ja/rest-resources.md b/docs/guide-ja/rest-resources.md index a6fcb75..07d508c 100644 --- a/docs/guide-ja/rest-resources.md +++ b/docs/guide-ja/rest-resources.md @@ -81,8 +81,8 @@ public function fields() ]; } -// いくつかのフィールドを除去する方法。親の実装を継承しつつ、公開すべきでないフィールドを -// 除外したいときに適している。 +// いくつかのフィールドを除去する方法。親の実装を継承しつつ、 +// 公開すべきでないフィールドを除外したいときに適している。 public function fields() { $fields = parent::fields(); diff --git a/docs/guide-ja/rest-response-formatting.md b/docs/guide-ja/rest-response-formatting.md index 641902b..b121fa7 100644 --- a/docs/guide-ja/rest-response-formatting.md +++ b/docs/guide-ja/rest-response-formatting.md @@ -154,7 +154,7 @@ JSON 形式のレスポンスを生成する [[yii\web\JsonResponseFormatter|Jso 例えば、[[yii\web\JsonResponseFormatter::$prettyPrint|$prettyPrint]] オプションは、より読みやすいレスポンスのためのもので、開発時に有用なオプションです。 また、[[yii\web\JsonResponseFormatter::$encodeOptions|$encodeOptions]] によって JSON エンコーディングの出力を制御することが出来ます。 -フォーマッタは、以下のように、アプリケーションの [構成情報](concept-configuration.md) の中で、`response` アプリケーション・コンポーネントの [[yii\web\Response::formatters|formatters]] プロパティの中で構成することが出来ます。 +フォーマッタは、以下のように、アプリケーションの [構成情報](concept-configurations.md) の中で、`response` アプリケーション・コンポーネントの [[yii\web\Response::formatters|formatters]] プロパティの中で構成することが出来ます。 ```php 'response' => [ diff --git a/docs/guide-ja/runtime-logging.md b/docs/guide-ja/runtime-logging.md index 8bedd89..2f761d6 100644 --- a/docs/guide-ja/runtime-logging.md +++ b/docs/guide-ja/runtime-logging.md @@ -142,8 +142,8 @@ Yii は下記のログ・ターゲットをあらかじめ内蔵しています [[yii\log\Target::categories|categories]] プロパティを指定しない場合は、 ターゲットが *全ての* カテゴリのメッセージを処理することを意味します。 -カテゴリを [[yii\log\Target::categories|categories]] プロパティでホワイト・リストとして登録する以外に、 -一定のカテゴリを [[yii\log\Target::except|except]] プロパティによってブラック・リストとして登録することも可能です。 +処理するカテゴリを [[yii\log\Target::categories|categories]] プロパティで指定する以外に、 +処理から除外するカテゴリを [[yii\log\Target::except|except]] プロパティによって指定することも可能です。 カテゴリの名前がこの配列にあるか、または配列にあるパターンに合致する場合は、メッセージはターゲットによって処理されません。 次のターゲットの構成は、ターゲットが、`yii\db\*` または `yii\web\HttpException:*` に合致するカテゴリ名を持つエラーおよび警告のメッセージだけを処理すべきこと、 diff --git a/docs/guide-ja/runtime-requests.md b/docs/guide-ja/runtime-requests.md index a74830d..4725d41 100644 --- a/docs/guide-ja/runtime-requests.md +++ b/docs/guide-ja/runtime-requests.md @@ -200,3 +200,27 @@ Yii アプリケーションに渡されるからです。 信頼できるプロキシからのリクエストである場合にのみ、`X-ProxyUser-Ip` と `Front-End-Https` ヘッダが受け入れられます。 その場合、前者は `ipHeaders` で構成されているようにユーザの IP を読み出すために使用され、 後者は [[yii\web\Request::getIsSecureConnection()]] の結果を決定するために使用されます。 + +2.0.31 以降、[RFC 7239](https://tools.ietf.org/html/rfc7239) の `Forwarded` ヘッダがサポートされています。 +有効にするためには、ヘッダ名を `secureHeaders` に追加する必要があります。 +あなたのプロキシにそれを設定させることを忘れないで下さい。さもないと、エンド・ユーザが IP とプロトコルを盗み見ることが可能になります。 + +### 解決済みのユーザ IP + +ユーザの IP アドレスが Yii アプリケーション以前に解決済みである場合(例えば、`ngx_http_realip_module` など) は、 +`request` コンポーネントは下記の構成で正しく動作します。 + +```php +'request' => [ + // ... + 'trustedHosts' => [ + '0.0.0.0/0', + ], + 'ipHeaders' => [], +], +``` + +この場合、[[yii\web\Request::userIP|userIP]] の値は `$_SERVER['REMOTE_ADDR']` に等しくなります。 +同時に、HTTP ヘッダから解決されるプロパティも正しく動作します (例えば、[[yii\web\Request::getIsSecureConnection()]])。 + +> 注意: `trustedHosts=['0.0.0.0/0']` の設定は、全ての IP が信頼できることを前提としています。 diff --git a/docs/guide-ja/runtime-sessions-cookies.md b/docs/guide-ja/runtime-sessions-cookies.md index 716e8a1..1a5cf03 100644 --- a/docs/guide-ja/runtime-sessions-cookies.md +++ b/docs/guide-ja/runtime-sessions-cookies.md @@ -397,3 +397,9 @@ Yii 2.0.21 以降、[[yii\web\Cookie::sameSite]] 設定がサポートされて ``` > Note: 今はまだ `sameSite` 設定をサポートしていないブラウザもありますので、 [追加の CSRF 保護](security-best-practices.md#avoiding-csrf) を行うことを強く推奨します。 + +## セッションに関する php.ini の設定 + +[PHP マニュアル](https://www.php.net/manual/ja/session.security.ini.php) で示されているように、`php.ini` にはセッションのセキュリティに関する重要な設定があります。 +推奨される設定を必ず適用して下さい。特に、PHP インストールのデフォルトでは有効にされていない +`session.use_strict_mode` を有効にして下さい。 diff --git a/docs/guide-ja/start-databases.md b/docs/guide-ja/start-databases.md index e98e1d3..85ff735 100644 --- a/docs/guide-ja/start-databases.md +++ b/docs/guide-ja/start-databases.md @@ -178,8 +178,9 @@ class CountryController extends Controller 上記のコードを `controllers/CountryController.php` というファイルに保存します。 -`index` アクションは `Country::find()` を呼び出します。このアクティブ・レコードのメソッドは `country` テーブルから全てのデータを読み出すことが可能な DB クエリを構築します。 -一回のリクエストで返される国の数を制限するために、クエリは [[yii\data\Pagination]] オブジェクトの助けを借りてページ付けされます。 +最初に `index` アクションは `Country::find()` を呼び出します。この [find()](https://www.yiiframework.com/doc/api/2.0/yii-db-activerecord#find()-detail) メソッドが `country` テーブルからデータを取得するメソッドを提供する [ActiveQuery](https://www.yiiframework.com/doc/api/2.0/yii-db-activequery) クエリ・オブジェクトオブジェクトを生成します。 + +一回のリクエストで返される国の数を制限するために、クエリ・オブジェクトは [[yii\data\Pagination]] オブジェクトの助けを借りてページ付けされます。 `Pagination` オブジェクトは二つの目的に奉仕します。 * クエリによって表現される SQL 文に `offset` 句と `limit` 句をセットして、 @@ -187,8 +188,10 @@ class CountryController extends Controller * 次の項で説明されるように、一連のページ・ボタンからなるページャを ビューに表示するために使われます。 -コードの最後で、`index` アクションは `index` と言う名前のビューをレンダリングしています。 -このとき、国データだけでなく、そのページネーション情報がビューに渡されます。 +次に、[all()](https://www.yiiframework.com/doc/api/2.0/yii-db-activequery#all()-detail) メソッドがクエリ結果に基づいて全ての `country` レコードを返します。 + +コードの最後で、`index` アクションは `index` と言う名前のビューをレンダリングします。 +このときに、返された国データとそのページネーション情報がビューに渡されます。 ビューを作成する diff --git a/docs/guide-ja/start-hello.md b/docs/guide-ja/start-hello.md index 23209f0..cdf330d 100644 --- a/docs/guide-ja/start-hello.md +++ b/docs/guide-ja/start-hello.md @@ -56,7 +56,7 @@ Yii はコントローラ・クラスの中で、アクション・メソッド アクション ID は常に小文字で参照されます。 アクション ID が複数の単語を必要とするときは、単語がダッシュ (-) で連結されます (例えば、`create-comment`)。 アクション・メソッドの名前は、アクション ID からダッシュを全て削除し、各単語の先頭の文字を大文字にした結果に `action` という接頭辞を付けたものになります。 -例えば、アクション ID `create-comment` に対応するアクション・メソッド名は `actionCreateComment` となります。 +例えば、アクション ID `create-comment` はアクション・メソッド名 `actionCreateComment` に対応します。 私たちの例では、アクション・メソッドは `$message` というパラメータを取り、そのデフォルト値は `"こんにちは"` です (PHP で関数やメソッドの引数にデフォルト値を設定するのと全く同じ方法です)。 diff --git a/docs/guide-ja/start-installation.md b/docs/guide-ja/start-installation.md index a3077f3..0a53d4f 100644 --- a/docs/guide-ja/start-installation.md +++ b/docs/guide-ja/start-installation.md @@ -290,3 +290,93 @@ server { また、HTTPS サーバを走らせている場合には、安全な接続であることを Yii が正しく検知できるように、 `fastcgi_param HTTPS on;` を追加しなければならないことにも注意を払ってください。 + +### 推奨される NGINX Unit の構成 + +[NGINX Unit](https://unit.nginx.org/) と PHP 言語モジュールを使って Yii ベースのアプリを走らせることが出来ます。 +その構成のサンプルです。 + +```json +{ + "listeners": { + "*:80": { + "pass": "routes/yii" + } + }, + + "routes": { + "yii": [ + { + "match": { + "uri": [ + "!/assets/*", + "*.php", + "*.php/*" + ] + }, + + "action": { + "pass": "applications/yii/direct" + } + }, + { + "action": { + "share": "/path/to/app/web/", + "fallback": { + "pass": "applications/yii/index" + } + } + } + ] + }, + + "applications": { + "yii": { + "type": "php", + "user": "www-data", + "targets": { + "direct": { + "root": "/path/to/app/web/" + }, + + "index": { + "root": "/path/to/app/web/", + "script": "index.php" + } + } + } + } +} +``` + +また、自分の PHP 環境を [セットアップ](https://unit.nginx.org/configuration/#php) したり、この同じ構成でカスタマイズした `php.ini` を提供したりすることも出来ます。 + +### IIS の構成 + +ドキュメント・ルートが `path/to/app/web` フォルダを指し、PHP を実行するように構成された仮想ホスト (ウェブ・サイト) でアプリケーションをホストすることを推奨します。その `web` フォルダに `web.config` という名前のファイル、すなわち `path/to/app/web/web.config` を配置しなければなりません。ファイルの内容は以下の通りです。 + +```xml + + + + + + + + + + + + + + + + + + +``` +また、IIS 上で PHP を構成するためには、以下にリストした Microsoft の公式リソースが有用でしょう。 + 1. [IIS の最初の Web サイトを構成する方法](https://support.microsoft.com/ja-jp/help/323972/how-to-set-up-your-first-iis-web-site) + 2. [Configure a PHP Website on IIS](https://docs.microsoft.com/en-us/iis/application-frameworks/scenario-build-a-php-website-on-iis/configure-a-php-website-on-iis) diff --git a/docs/guide-ja/start-looking-ahead.md b/docs/guide-ja/start-looking-ahead.md index fb893a5..9f0ef20 100644 --- a/docs/guide-ja/start-looking-ahead.md +++ b/docs/guide-ja/start-looking-ahead.md @@ -27,7 +27,7 @@ Gii をコード生成に使うと、ウェブ開発のプロセスの大部分 * コミュニティ - フォーラム: - IRC チャット: freenode ネットワーク () の #yii チャンネル - - Slack チャンネル: + - Slack チャンネル: - Gitter チャット: - GitHub: - Facebook: diff --git a/docs/guide-ja/tutorial-mailing.md b/docs/guide-ja/tutorial-mailing.md index 42e381f..5c8e9dd 100644 --- a/docs/guide-ja/tutorial-mailing.md +++ b/docs/guide-ja/tutorial-mailing.md @@ -23,6 +23,15 @@ return [ 'components' => [ 'mailer' => [ 'class' => 'yii\swiftmailer\Mailer', + 'useFileTransport' => false, + 'transport' => [ + 'class' => 'Swift_SmtpTransport', + 'encryption' => 'tls', + 'host' => 'your_mail_server_host', + 'port' => 'your_smtp_port', + 'username' => 'your_username', + 'password' => 'your_password', + ], ], ], ]; diff --git a/docs/guide-ja/tutorial-performance-tuning.md b/docs/guide-ja/tutorial-performance-tuning.md index d0543d4..8efb7ce 100644 --- a/docs/guide-ja/tutorial-performance-tuning.md +++ b/docs/guide-ja/tutorial-performance-tuning.md @@ -222,4 +222,4 @@ composer dumpautoload -o ## アプリケーションをスケーラブルなものにする覚悟を決める -何をやっても助けにならないときは、あなたのアプリケーションをスケーラブルにすることを試みましょう。良い導入記事が [Configuring a Yii 2 Application for an Autoscaling Stack (Yii 2 アプリケーションを自動スケール環境のために構成する)](https://github.com/samdark/yii2-cookbook/blob/master/book/scaling.md) の中で提供されています。更に詳しく知りたい場合は [Web apps performance and scaling (ウェブ・アプリのパフォーマンスとスケーリング)](http://thehighload.com/) を参照して下さい。 +何をやっても助けにならないときは、あなたのアプリケーションをスケーラブルにすることを試みましょう。良い導入記事が [Configuring a Yii 2 Application for an Autoscaling Stack (Yii 2 アプリケーションを自動スケール環境のために構成する)](https://github.com/samdark/yii2-cookbook/blob/master/book/scaling.md) の中で提供されています。 diff --git a/docs/guide-pl/input-validation.md b/docs/guide-pl/input-validation.md index e705be5..7327a66 100644 --- a/docs/guide-pl/input-validation.md +++ b/docs/guide-pl/input-validation.md @@ -319,8 +319,10 @@ Wbudowany walidator jest zdefiniowaną w modelu metodą lub funkcją anonimową. * @param mixed $params wartość parametru podanego w zasadzie walidacji * @param \yii\validators\InlineValidator $validator powiązana instancja InlineValidator * Ten parametr jest dostępny od wersji 2.0.11. + * @param mixed $current aktualnie walidowana wartość atrybutu. + * Ten parametr jest dostępny od wersji 2.0.36. */ -function ($attribute, $params, $validator) +function ($attribute, $params, $validator, $current) ``` Jeśli atrybut nie przejdzie walidacji, metoda/funkcja powinna wywołać metodę [[yii\base\Model::addError()|addError()]] do zapisania wiadomości o błędzie w modelu, diff --git a/docs/guide-pl/start-forms.md b/docs/guide-pl/start-forms.md index 434c3d4..7a76ad5 100644 --- a/docs/guide-pl/start-forms.md +++ b/docs/guide-pl/start-forms.md @@ -12,7 +12,6 @@ W tym poradniku nauczysz się jak: * utworzyć [model](structure-models.md) reprezentujący dane wprowadzone przez użytkownika przez formularz, * zadeklarować zasady do sprawdzenia wprowadzonych danych, * zbudować formularz HTML w [widoku](structure-views.md). -* build an HTML form in a [view](structure-views.md). Tworzenie modelu @@ -228,4 +227,4 @@ W tej sekcji poradnika dotknęliśmy każdej części struktury MVC. Nauczyłeś Nauczyłeś się także, jak pobierać dane od użytkowników oraz jak wyświetlać pobrane dane w przeglądarce. To zadanie mogłoby zabrać Ci wiele czasu podczas pisania aplikacji, jednak Yii dostarcza wiele widżetów, które bardzo je ułatwiają. -W następnej sekcji nauczysz się pracy z bazą danych, która jest wymagana w niemalże każdej aplikacji. \ No newline at end of file +W następnej sekcji nauczysz się pracy z bazą danych, która jest wymagana w niemalże każdej aplikacji. diff --git a/docs/guide-ru/caching-data.md b/docs/guide-ru/caching-data.md index 4c27378..1dab06a 100644 --- a/docs/guide-ru/caching-data.md +++ b/docs/guide-ru/caching-data.md @@ -32,7 +32,7 @@ $data = $cache->getOrSet($key, function () { ``` Если в кэше есть данные по ключу `$key`, они будут сразу возвращены. -Иначе, будет вызвана переданная анонимная функция, вычисляющаяя значение, которое будет сохранено в кэш и возвращено +Иначе, будет вызвана переданная анонимная функция, вычисляющая значение, которое будет сохранено в кэш и возвращено из метода. В случае, когда анонимной функции требуются данные из внешней области видимости, можно передать их с помощью @@ -45,7 +45,7 @@ $data = $cache->getOrSet($key, function () use ($user_id) { }); ``` -> Note: В [[yii\caching\Cache::getOrSet()|getOrSet()]] можно передать срока действия и зависимости кэша. +> Note: В [[yii\caching\Cache::getOrSet()|getOrSet()]] можно передать срок действия и зависимости кэша. Прочтите [Срок действия кэша](#cache-expiration) и [Зависимости кеша](#cache-dependencies) чтобы узнать больше. diff --git a/docs/guide-ru/caching-fragment.md b/docs/guide-ru/caching-fragment.md index 9e263fa..09c43ad 100644 --- a/docs/guide-ru/caching-fragment.md +++ b/docs/guide-ru/caching-fragment.md @@ -17,9 +17,9 @@ if ($this->beginCache($id)) { Таким образом заключите то, что вы хотите закэшировать между вызовом [[yii\base\View::beginCache()|beginCache()]] и [[yii\base\View::endCache()|endCache()]]. Если содержимое будет найдено в кэше, [[yii\base\View::beginCache()|beginCache()]] отобразит закэшированное содержимое и вернёт `false`, минуя генерацию содержимого. -В противном случае, будет выполнен код генерации контента и когда будет вызван [[yii\base\View::endCache()|endCache()]], то сгенерированное содержимое будет записано и сохранено в кэше. +В противном случае будет выполнен код генерации контента и когда будет вызван [[yii\base\View::endCache()|endCache()]], то сгенерированное содержимое будет записано и сохранено в кэше. -Также как и [кэширование данных](caching-data.md), для кэширования фрагментов требуется уникальный идентификатор для определения кэшируемого фрагмента. +Так же как и [кэширование данных](caching-data.md), для кэширования фрагментов требуется уникальный идентификатор для определения кэшируемого фрагмента. ## Параметры кэширования @@ -45,7 +45,7 @@ if ($this->beginCache($id, ['duration' => 3600])) { ### Зависимости -Также как и [кэширование данных](caching-data.md#cache-dependencies), кэшируемое содержимое фрагмента тоже может иметь зависимости. Например, отображение содержимого сообщения зависит от того, изменено или нет это сообщение. +Так же как и [кэширование данных](caching-data.md#cache-dependencies), кэшируемое содержимое фрагмента тоже может иметь зависимости. Например, отображение содержимого сообщения зависит от того, изменено или нет это сообщение. Для определения зависимости мы устанавливаем параметр [[yii\widgets\FragmentCache::dependency|dependency]], который может быть либо объектом [[yii\caching\Dependency]], либо массивом настроек, который может быть использован для создания объекта [[yii\caching\Dependency]]. Следующий код определяет содержимое фрагмента, зависящее от изменения значения столбца `updated_at`: diff --git a/docs/guide-ru/caching-http.md b/docs/guide-ru/caching-http.md index 20e9973..d0ae15a 100644 --- a/docs/guide-ru/caching-http.md +++ b/docs/guide-ru/caching-http.md @@ -12,7 +12,7 @@ HTTP кэширование ## Заголовок `Last-Modified` -Заголовок `Last-Modified` использует временную метку timestamp, чтобы показать была ли страница изменена после того, как клиент закэшировал её. +Заголовок `Last-Modified` использует временную метку timestamp, чтобы показать, была ли страница изменена после того, как клиент закэшировал её. Вы можете настроить свойство [[yii\filters\HttpCache::lastModified]], чтобы включить отправку заголовка `Last-Modified`. Свойство должно содержать PHP-функцию, возвращающую временную метку UNIX timestamp времени последнего изменения страницы. Сигнатура PHP-функции должна совпадать со следующей, @@ -44,7 +44,7 @@ public function behaviors() ``` Приведенный выше код устанавливает, что HTTP кэширование должно быть включено только для действия `index`. Он -генерирует `Last-Modified` HTTP заголовок на основе времени последнего сообщения. Когда браузер в первый раз посещает страницу `index`, то страница будет сгенерирована на сервере и отправлена в браузер; если браузер снова зайдёт на эту страницу и с тех пор ни один пост не обновится, то сервер не будет пересоздавать страницу и браузер будет использовать закэшированную на стороне клиента версию. В результате, будет пропущено как создание страницы на стороне сервера, так и передача содержания страницы клиенту. +генерирует `Last-Modified` HTTP заголовок на основе времени последнего сообщения. Когда браузер в первый раз посещает страницу `index`, то страница будет сгенерирована на сервере и отправлена в браузер; если браузер снова зайдёт на эту страницу и с тех пор ни один пост не обновится, то сервер не будет пересоздавать страницу и браузер будет использовать закэшированную на стороне клиента версию. В результате будет пропущено как создание страницы на стороне сервера, так и передача содержания страницы клиенту. ## Заголовок `ETag` @@ -81,7 +81,7 @@ public function behaviors() ``` Приведенный выше код устанавливает, что HTTP кэширование должно быть включено только для действия `view`. Он -генерирует `ETag` HTTP заголовок на основе заголовка и содержания последнего сообщения. Когда браузер в первый раз посещает страницу `view`, то страница будет сгенерирована на сервере и отправлена в браузер; если браузер снова зайдёт на эту страницу и с тех пор ни один пост не обновится, то сервер не будет пересоздавать страницу и браузер будет использовать закэшированную на стороне клиента версию. В результате, будет пропущено как создание страницы на стороне сервера, так и передача содержание страницы клиенту. +генерирует `ETag` HTTP заголовок на основе заголовка и содержания последнего сообщения. Когда браузер в первый раз посещает страницу `view`, то страница будет сгенерирована на сервере и отправлена в браузер; если браузер снова зайдёт на эту страницу и с тех пор ни один пост не обновится, то сервер не будет пересоздавать страницу и браузер будет использовать закэшированную на стороне клиента версию. В результате будет пропущено как создание страницы на стороне сервера, так и передача содержание страницы клиенту. ETags позволяет применять более сложные и/или более точные стратегии кэширования, чем заголовок `Last-Modified`. Например, ETag станет невалидным (некорректным), если на сайте была включена другая тема diff --git a/docs/guide-ru/caching-page.md b/docs/guide-ru/caching-page.md index f85a122..c368d2d 100644 --- a/docs/guide-ru/caching-page.md +++ b/docs/guide-ru/caching-page.md @@ -2,7 +2,7 @@ ================= Кэширование страниц — это кэширование всего содержимого страницы на стороне сервера. Позже, когда эта страница -будет снова запрошена, сервер вернет её из кэша вместо того чтобы генерировать её заново. +будет снова запрошена, сервер вернет её из кэша вместо того, чтобы генерировать её заново. Кэширование страниц осуществляется при помощи [фильтра действия](structure-filters.md) [[yii\filters\PageCache]] и может быть использовано в классе контроллера следующим образом: diff --git a/docs/guide-ru/concept-aliases.md b/docs/guide-ru/concept-aliases.md index d3d8dac..6f38c73 100644 --- a/docs/guide-ru/concept-aliases.md +++ b/docs/guide-ru/concept-aliases.md @@ -97,7 +97,7 @@ $cache = new FileCache([ ]); ``` -Для того, чтобы узнать поддерживает ли метод или свойство псевдонимы, обратитесь к документации API. +Для того, чтобы узнать, поддерживает ли метод или свойство псевдонимы, обратитесь к документации API. Заранее определённые псевдонимы diff --git a/docs/guide-ru/concept-autoloading.md b/docs/guide-ru/concept-autoloading.md index 92176bf..f24e016 100644 --- a/docs/guide-ru/concept-autoloading.md +++ b/docs/guide-ru/concept-autoloading.md @@ -75,7 +75,7 @@ require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php'; ``` -Вы можете использовать автозагрузчик Composer без автозагрузчика Yii. Однако, скорость автозагрузки в этом случае +Вы можете использовать автозагрузчик Composer без автозагрузчика Yii. Однако скорость автозагрузки в этом случае может уменьшится. Также вам будет необходимо следовать правилам автозагрузчика Composer. > Info: Если вы не хотите использовать автозагрузчик Yii, создайте свою версию файла `Yii.php` diff --git a/docs/guide-ru/concept-behaviors.md b/docs/guide-ru/concept-behaviors.md index b9749c5..de7126d 100644 --- a/docs/guide-ru/concept-behaviors.md +++ b/docs/guide-ru/concept-behaviors.md @@ -348,7 +348,7 @@ $user->touch('login_time'); ### Плюсы поведений -Поведения, как и любые другие классы, поддерживают наследование. Трейты же можно рассматривать как копипейст +Поведения, как и любые другие классы, поддерживают наследование. Трейты можно рассматривать как копипейст на уровне языка. Они наследование не поддерживают. Поведения могут быть прикреплены и отвязаны от компонента динамически, без необходимости модифицирования класса diff --git a/docs/guide-ru/concept-configurations.md b/docs/guide-ru/concept-configurations.md index 360ef1f..c97a114 100644 --- a/docs/guide-ru/concept-configurations.md +++ b/docs/guide-ru/concept-configurations.md @@ -55,7 +55,7 @@ Yii::configure($object, $config); созданные через геттеры и сеттеры. * Элементы `on eventName` указывают на то, какие обработчики должны быть прикреплены к [событиям](concept-events.md) объекта. Обратите внимание, что ключи массива начинаются с `on `. Чтобы узнать весь список поддерживаемых видов - обработчиков событий обратитесь в раздел [события](concept-events.md) + обработчиков событий, обратитесь в раздел [события](concept-events.md) * Элементы `as behaviorName` указывают на то, какие [поведения](concept-behaviors.md) должны быть внедрены в объект. Обратите внимание, что ключи массива начинаются с `as `; а `$behaviorConfig` представляет собой конфигурацию для создания [поведения](concept-behaviors.md), похожую на все остальные конфигурации. diff --git a/docs/guide-ru/concept-di-container.md b/docs/guide-ru/concept-di-container.md index e4106ce..5ad5361 100644 --- a/docs/guide-ru/concept-di-container.md +++ b/docs/guide-ru/concept-di-container.md @@ -110,7 +110,7 @@ $container->get('Foo', [], [ Допустим, мы работаем над API и у нас есть: - `app\components\Request`, наследуемый от `yii\web\Request` и реализующий дополнительные возможности. -- `app\components\Response`, наследуемый от `yii\web\Response` с свойством `format`, по умолчанию инициализируемом как `json`. +- `app\components\Response`, наследуемый от `yii\web\Response` со свойством `format`, по умолчанию инициализируемом как `json`. - `app\storage\FileStorage` и `app\storage\DocumentsReader`, где реализована некая логика для работы с документами в неком файловом хранилище: @@ -358,8 +358,11 @@ $container->setSingleton('yii\db\Connection', [ // "db" ранее зарегистрированный псевдоним $db = $container->get('db'); -// эквивалентно: $engine = new \app\components\SearchEngine($apiKey, ['type' => 1]); -$engine = $container->get('app\components\SearchEngine', [$apiKey], ['type' => 1]); +// эквивалентно: $engine = new \app\components\SearchEngine($apiKey, $apiSecret, ['type' => 1]); +$engine = $container->get('app\components\SearchEngine', [$apiKey, $apiSecret], ['type' => 1]); + +// эквивалентно: $api = new \app\components\Api($host, $apiKey); +$api = $container->get('app\components\Api', ['host' => $host, 'apiKey' => $apiKey]); ``` За кулисами, контейнер внедрения зависимостей делает гораздо больше работы, чем просто создание нового объекта. diff --git a/docs/guide-ru/concept-events.md b/docs/guide-ru/concept-events.md index 83413eb..f9eb47d 100644 --- a/docs/guide-ru/concept-events.md +++ b/docs/guide-ru/concept-events.md @@ -280,7 +280,7 @@ Event::trigger(Dog::className(), DanceEventInterface::EVENT_DANCE); Event::trigger(Developer::className(), DanceEventInterface::EVENT_DANCE); ``` -Однако, невозможно инициализировать событие во всех классах, которые реализуют интерфейс: +Однако невозможно инициализировать событие во всех классах, которые реализуют интерфейс: ```php // НЕ БУДЕТ РАБОТАТЬ diff --git a/docs/guide-ru/concept-service-locator.md b/docs/guide-ru/concept-service-locator.md index ce48c19..2f90f8b 100644 --- a/docs/guide-ru/concept-service-locator.md +++ b/docs/guide-ru/concept-service-locator.md @@ -8,7 +8,7 @@ Service Locator является объектом, предоставляющи В Yii Service Locator является экземпляром класса [[yii\di\ServiceLocator]] или его дочернего класса. Наиболее часто используемый Service Locator в Yii — это объект *приложения*, который можно получить через `\Yii::$app`. -Предоставляемые им службы, такие, как компоненты `request`, `response`, `urlManager`, называют *компонентами приложения*. +Предоставляемые им службы такие, как компоненты `request`, `response`, `urlManager`, называют *компонентами приложения*. Благодаря Service Locator вы легко можете настроить эти компоненты или даже заменить их собственными реализациями. Помимо объекта приложения, объект каждого модуля также является Service Locator. diff --git a/docs/guide-ru/db-active-record.md b/docs/guide-ru/db-active-record.md index 5436bfd..2602fd6 100644 --- a/docs/guide-ru/db-active-record.md +++ b/docs/guide-ru/db-active-record.md @@ -32,7 +32,7 @@ Yii поддерживает работу с Active Record для следующ * SQLite 2 и 3: посредством [[yii\db\ActiveRecord]] * Microsoft SQL Server 2008 и выше: посредством [[yii\db\ActiveRecord]] * Oracle: посредством [[yii\db\ActiveRecord]] -* CUBRID 9.3 и выше: посредством [[yii\db\ActiveRecord]] (Имейте ввиду, что вследствие +* CUBRID 9.3 и выше: посредством [[yii\db\ActiveRecord]] (Имейте в виду, что вследствие [бага](http://jira.cubrid.org/browse/APIS-658) в PDO-расширении для CUBRID, заключение значений в кавычки не работает, поэтому необходимо использовать CUBRID версии 9.3 как на клиентской стороне, так и на сервере) * Sphinx: посредством [[yii\sphinx\ActiveRecord]], потребуется расширение `yii2-sphinx` @@ -61,7 +61,7 @@ Yii поддерживает работу с Active Record для следующ [[yii\db\Connection::$tablePrefix|tablePrefix]] задан как `tbl_`, `Customer` преобразуется в `tbl_customer`, а `OrderItem` в `tbl_order_item`. -Если имя таблицы указано в формате `{{%TableName}}`, символ `%` заменяется префиксом. Например, , `{{%post}}` становится +Если имя таблицы указано в формате `{{%TableName}}`, символ `%` заменяется префиксом. Например `{{%post}}` становится `{{tbl_post}}`. Фигуриные скобки используются для [экранирования в SQL-запросах](db-dao.md#quoting-table-and-column-names). В нижеследующем примере мы объявляем класс Active Record с названием `Customer` для таблицы `customer`. @@ -385,7 +385,7 @@ $customer->save(); Метод [[yii\db\ActiveRecord::save()|save()]] может вставить или обновить строку данных в зависимости от состояния Active Record объекта. Если объект создан с помощью оператора `new`, вызов метода [[yii\db\ActiveRecord::save()|save()]] -приведёт к вставке новой строки данных; если же объект был получен с помощью запроса на получение данных, вызов +приведёт к вставке новой строки данных; если объект был получен с помощью запроса на получение данных, вызов [[yii\db\ActiveRecord::save()|save()]] обновит строку таблицы, соответствующую объекту Active Record. Вы можете различать два состояния Active Record объекта с помощью проверки значения его свойства @@ -841,7 +841,7 @@ class Order extends ActiveRecord ключи массива - столбцы связанных данных. Есть простой способ запомнить это правило: как вы можете увидеть в примере выше, столбец связной Active Record - указывается сразу же после указания самого класса Active Record. Вы видите, что `customer_id` - это свойство класса + указывается сразу после указания самого класса Active Record. Вы видите, что `customer_id` - это свойство класса `Order`, а `id` - свойство класса `Customer`. @@ -932,7 +932,7 @@ $orders = $customer->bigOrders; При проектировании баз данных, когда между двумя таблицами имеется кратность связи many-to-many, обычно вводится [промежуточная таблица](http://en.wikipedia.org/wiki/Junction_table). Например, таблицы `order` и `item` могут быть -связаны посредством промежуточной таблицы с названием `order_item`. Один заказ будет соотносится с несколькими товарами, +связаны посредством промежуточной таблицы с названием `order_item`. Один заказ будет соотноситься с несколькими товарами, в то время как один товар будет также соотноситься с несколькими заказами. При объявлении подобных связей вы можете пользоваться методом [[yii\db\ActiveQuery::via()|via()]] или методом @@ -1147,7 +1147,7 @@ $customers = Customer::find() По умолчанию, метод [[yii\db\ActiveQuery::joinWith()|joinWith()]] будет использовать конструкцию `LEFT JOIN` для объединения основной таблицы со связной. Вы можете указать другой тип операции JOIN (например, `RIGHT JOIN`) с помощью -третьего параметра этого метода - `$joinType`. Если же вам нужен `INNER JOIN`, вы можете вместо этого просто вызвать +третьего параметра этого метода - `$joinType`. Если вам нужен `INNER JOIN`, вы можете вместо этого просто вызвать метод [[yii\db\ActiveQuery::innerJoinWith()|innerJoinWith()]]. Вызов метода [[yii\db\ActiveQuery::joinWith()|joinWith()]] будет [жадно загружать](#lazy-eager-loading) связные данные @@ -1214,7 +1214,7 @@ $query->joinWith([ $query->joinWith(['orders o'])->orderBy('o.id'); ``` -Этот синтаксис работает для простых связей. Если же необходимо использовать связующую таблицу, например +Этот синтаксис работает для простых связей. Если необходимо использовать связующую таблицу, например `$query->joinWith(['orders.product'])`, то вызовы joinWith вкладываются друг в друга: ```php diff --git a/docs/guide-ru/db-dao.md b/docs/guide-ru/db-dao.md index f464617..9fbeae1 100644 --- a/docs/guide-ru/db-dao.md +++ b/docs/guide-ru/db-dao.md @@ -202,7 +202,7 @@ $post2 = $command->queryOne(); ``` Обратите внимание что вы связываете маркер `$id` с переменной перед выполнением запроса, и затем меняете это значение -перед каждым последующим выполнением (часто это делается в цикле). Выполнении запросов таким образом может быть значительно +перед каждым последующим выполнением (часто это делается в цикле). Выполнение запросов таким образом может быть значительно более эффективным, чем выполнение запроса для каждого значения параметра. ### Выполнение Не-SELECT запросов @@ -545,7 +545,7 @@ Yii::$app->db->createCommand("UPDATE user SET username='demo' WHERE id=1")->exec ``` Конфигурация выше, определяет два основных и четыре подчинённых серверов. Компонент `Connection` поддерживает -балансировку нагрузки и переключение при сбое между основными серверами, также как и между подчинёнными. Различие +балансировку нагрузки и переключение при сбое между основными серверами, так же как и между подчинёнными. Различие заключается в том, что когда ни к одному из основных серверов не удастся подключиться будет выброшено исключение. > Note: Когда вы используете свойство [[yii\db\Connection::masters|masters]] для настройки одного или нескольких @@ -596,7 +596,7 @@ $rows = Yii::$app->db->useMaster(function ($db) { ## Работа со схемой базы данных Yii DAO предоставляет целый набор методов для управления схемой базы данных, таких как создание новых таблиц, удаление -столбцов из таблицы, и т.д.. Эти методы описаны ниже: +столбцов из таблицы, и т.д. Эти методы описаны ниже: * [[yii\db\Command::createTable()|createTable()]]: создание таблицы * [[yii\db\Command::renameTable()|renameTable()]]: переименование таблицы @@ -632,5 +632,5 @@ $table = Yii::$app->db->getTableSchema('post'); ``` Метод вернёт объект [[yii\db\TableSchema]], который содержит информацию о столбцах таблицы, первичных ключах, внешних -ключах, и т.д.. Вся эта информация используется главным образом для [построителя запросов](db-query-builder.md) и +ключах, и т.д. Вся эта информация используется главным образом для [построителя запросов](db-query-builder.md) и [active record](db-active-record.md), чтоб помочь вам писать независимый от базы данных код. diff --git a/docs/guide-ru/db-migrations.md b/docs/guide-ru/db-migrations.md index 0793f4a..24d1091 100644 --- a/docs/guide-ru/db-migrations.md +++ b/docs/guide-ru/db-migrations.md @@ -47,7 +47,7 @@ yii migrate/create create_news_table > Note: Поскольку аргумент `name` будет использован как часть имени класса создаваемой миграции, он должен содержать только буквы, цифры и/или символы подчеркивания. -Приведенная выше команда создаст новый PHP класс с именем файла `m150101_185401_create_news_table.php` в директории `@app/migrations`. Файл содержит следующий код, который главным образом декларирует класс миграции `m150101_185401_create_news_table` с следующим каркасом кода: +Приведенная выше команда создаст новый PHP класс с именем файла `m150101_185401_create_news_table.php` в директории `@app/migrations`. Файл содержит следующий код, который главным образом декларирует класс миграции `m150101_185401_create_news_table` со следующим каркасом кода: ```php _` (`m<ГодМесяцДень_ЧасыМинутыСекунды>_<Имя>`), где * `` относится к UTC дате-времени при котором команда создания миграции была выполнена. -* `` это тоже самое значение аргумента `name` которое вы прописываете в команду. +* `` это то же самое значение аргумента `name` которое вы прописываете в команду. В классе миграции, вы должны прописать код в методе `up()` когда делаете изменения в структуре базы данных. Вы также можете написать код в методе `down()`, чтобы отменить сделанные `up()` изменения. Метод `up` вызывается для обновления базы данных с помощью данной миграции, а метод `down()` вызывается для отката изменений базы данных. @@ -446,7 +446,7 @@ class m150811_220037_drop_position_column_from_post_table extends Migration ### Добавление промежуточной таблицы -Если имя миграции задано как `create_junction_table_for_xxx_and_yyy_tables`, файл будет содержать код для создания промежуточной таблцы. +Если имя миграции задано как `create_junction_table_for_xxx_and_yyy_tables`, файл будет содержать код для создания промежуточной таблицы. ```php yii migrate/create create_junction_table_for_post_and_tag_tables @@ -553,7 +553,7 @@ class m160328_041642_create_junction_table_for_post_and_tag_tables extends Migra При выполнении сложных миграций баз данных, важно обеспечить каждую миграцию либо успехом, либо ошибкой, в целом так, чтобы база данных могла поддерживать целостность и непротиворечивость. Для достижения данной цели рекомендуется, заключить операции каждой миграции базы данных в [транзакции](db-dao.md#performing-transactions). -Самый простой способ реализации транзакций миграций это прописать код миграций в методы `safeUp()` и `safeDown()`. Эти два метода отличаются от методов `up()` и `down()` тем, что они неявно заключены в транзакции. В результате, если какая-либо операция в этих методах не удается, все предыдущие операции будут отменены автоматически. +Самый простой способ реализации транзакций миграций это прописать код миграций в методы `safeUp()` и `safeDown()`. Эти два метода отличаются от методов `up()` и `down()` тем, что они неявно заключены в транзакции. В результате если какая-либо операция в этих методах не удается, все предыдущие операции будут отменены автоматически. В следующем примере, помимо создания таблицы `news` мы также вставляем в этой таблице начальную строку. @@ -628,7 +628,7 @@ class m150101_185401_create_news_table extends Migration > Info: [[yii\db\Migration]] не предоставляет методы запросов к базе данных. Это потому, что обычно не требуется отображать дополнительные сообщения об извлечении данных из базы данных. Это также, потому, что можно использовать более мощный [Построитель Запросов](db-query-builder.md) для построения и выполнения сложных запросов. -> Note: при обработке данных внутри миграции, может показаться, что использование существующих классов [Active Record](db-active-record.md), со всей их готовой бизнес логикой, будет разумным решением и упросит код миграции. Однако, следует помнить, что код миграций не должен меняться, по определению. В отличии от миграций, бизнес логика приложений часто изменяется. Это может привести к нарушению работы миграции при определённых изменениях на уровне Active Record. Поэтому рекомендуется делать миграции независимыми от других частей приложения, таких как классы Active Record. +> Note: при обработке данных внутри миграции, может показаться, что использование существующих классов [Active Record](db-active-record.md), со всей их готовой бизнес логикой, будет разумным решением и упросит код миграции. Однако следует помнить, что код миграций не должен меняться, по определению. В отличие от миграций, бизнес логика приложений часто изменяется. Это может привести к нарушению работы миграции при определённых изменениях на уровне Active Record. Поэтому рекомендуется делать миграции независимыми от других частей приложения, таких как классы Active Record. ## Применение Миграций @@ -731,7 +731,7 @@ yii migrate/mark 1392853618 # используя вре Обратите внимание, что данный каталог должен существовать, иначе команда будет выдавать ошибку. * `migrationTable`: строка - string (по умолчанию `migration`). Определяет имя таблицы в базе данных в которой хранится - информация о истории миграций. Эта таблица будет автоматически создана командой миграции, если её не существует. + информация об истории миграций. Эта таблица будет автоматически создана командой миграции, если её не существует. Вы также можете создать её вручную, используя структуру `version varchar(255) primary key, apply_time integer`. * `db`: строка - string (по умолчанию `db`). Определяет ID базы данных [компонента приложения](structure-application-components.md). diff --git a/docs/guide-ru/db-query-builder.md b/docs/guide-ru/db-query-builder.md index 4a87021..5db7896 100644 --- a/docs/guide-ru/db-query-builder.md +++ b/docs/guide-ru/db-query-builder.md @@ -384,7 +384,7 @@ $query->orderBy('id ASC, name DESC'); > Note: Вы должны использовать массив для указания `ORDER BY` содержащих выражения БД. -Вы можете вызывать [[yii\db\Query::addOrderBy()|addOrderBy()]] для добавления столбцов в фрагмент `ORDER BY`. +Вы можете вызывать [[yii\db\Query::addOrderBy()|addOrderBy()]] для добавления столбцов во фрагмент `ORDER BY`. ```php $query->orderBy('id ASC') @@ -410,8 +410,8 @@ $query->groupBy('id, status'); > Note: Вы должны использовать массив для указания `GROUP BY` содержащих выражения БД. -Вы можете вызывать [[yii\db\Query::addGroupBy()|addGroupBy()]] для добавления имён столбцов в фрагмент `GROUP BY`. -For example, +Вы можете вызывать [[yii\db\Query::addGroupBy()|addGroupBy()]] для добавления имён столбцов во фрагмент `GROUP BY`. +Например, ```php $query->groupBy(['id', 'status']) @@ -429,10 +429,10 @@ $query->groupBy(['id', 'status']) $query->having(['status' => 1]); ``` -Пожалуйста, обратитесь к документации для [where()](#where) для более подробной информации о определении условий. +Пожалуйста, обратитесь к документации для [where()](#where) для более подробной информации об определении условий. Вы можете вызывать [[yii\db\Query::andHaving()|andHaving()]] или [[yii\db\Query::orHaving()|orHaving()]] для добавления -дополнительных условий в фрагмент `HAVING`. +дополнительных условий во фрагмент `HAVING`. ```php // ... HAVING (`status` = 1) AND (`age` > 30) @@ -471,7 +471,7 @@ $query->join('LEFT JOIN', 'post', 'post.user_id = user.id'); - `$type`: тип объединения, например, `'INNER JOIN'`, `'LEFT JOIN'`. - `$table`: имя таблицы, которая должна быть присоединена. - `$on`: необязательное условие объединения, то есть фрагмент `ON`. Пожалуйста, обратитесь к документации для - [where()](#where) для более подробной информации о определении условий. Отметим, что синтаксис массивов **не работает** + [where()](#where) для более подробной информации об определении условий. Отметим, что синтаксис массивов **не работает** для задания условий для столбцов, то есть `['user.id' => 'comment.userId']` будет означать условие, где ID пользователя должен быть равен строке `'comment.userId'`. Вместо этого стоит указывать условие в виде строки `'user.id = comment.userId'`. - `$params`: необязательные параметры присоединяемые к условию объединения. @@ -498,7 +498,7 @@ $subQuery = (new \yii\db\Query())->from('post'); $query->leftJoin(['u' => $subQuery], 'u.id = author_id'); ``` -В этом случае, вы должны передать подзапросы в массиве и использовать ключи для определения алиасов. +В этом случае вы должны передать подзапросы в массиве и использовать ключи для определения алиасов. ### [[yii\db\Query::union()|union()]] @@ -554,7 +554,7 @@ $row = (new \yii\db\Query()) > Note: метод [[yii\db\Query::one()|one()]] вернёт только первую строку результата запроса. Он НЕ добавляет `LIMIT 1` в генерируемый SQL. Это хорошо и предпочтительно если вы знаете, что запрос вернёт только одну или несколько - строк данных (например, при запросе по первичному ключу). Однако, если запрос потенциально может вернут много + строк данных (например, при запросе по первичному ключу). Однако если запрос потенциально может вернут много строк данных, вы должны вызвать `limit(1)` для повышения производительности, Например, `(new \yii\db\Query())->from('user')->limit(1)->one()`. @@ -574,7 +574,7 @@ $count = (new \yii\db\Query()) При вызове методов выборки [[yii\db\Query]], внутри на самом деле проводится следующая работа: * Вызывается [[yii\db\QueryBuilder]] для генерации SQL запроса на основе текущего [[yii\db\Query]]; -* Создаёт объект [[yii\db\Command]] с сгенерированным SQL запросом; +* Создаёт объект [[yii\db\Command]] со сгенерированным SQL запросом; * Вызывается выбирающий метод (например [[yii\db\Command::queryAll()|queryAll()]]) из [[yii\db\Command]] для выполнения SQL запроса и извлечения данных. Иногда вы можете захотеть увидеть или использовать SQL запрос построенный из объекта [[yii\db\Query]]. Этой цели можно @@ -630,7 +630,7 @@ $query = (new \yii\db\Query()) ### Пакетная выборка -При работе с большими объемами данных, методы на подобие [[yii\db\Query::all()]] не подходят, потому что они требуют +При работе с большими объемами данных, методы наподобие [[yii\db\Query::all()]] не подходят, потому что они требуют загрузки всех данных в память. Чтобы сохранить требования к памяти минимальными, Yii предоставляет поддержку так называемых пакетных выборок. Пакетная выборка делает возможным курсоры данных и выборку данных пакетами. @@ -662,7 +662,7 @@ SQL запрос к базе данных. Данные будут выбира По сравнению с [[yii\db\Query::all()]], пакетная выборка загружает только по 100 строк данных за раз в память. Если вы обрабатываете данные и затем сразу выбрасываете их, пакетная выборка может помочь уменьшить использование памяти. -Если указать индексный столбец через [[yii\db\Query::indexBy()]], в пакетной выборке индекс будет сохранятся. +Если указать индексный столбец через [[yii\db\Query::indexBy()]], в пакетной выборке индекс будет сохраняться. Например, ```php diff --git a/docs/guide-ru/glossary.md b/docs/guide-ru/glossary.md index d09a183..a3c8b9d 100644 --- a/docs/guide-ru/glossary.md +++ b/docs/guide-ru/glossary.md @@ -2,13 +2,15 @@ ## alias -Alias это строка которую Yii использует чтобы указывать на класс или директорию, например '@app/vendor'. +Alias - это строка, с помощью которой Yii указывает на класс или директорию, например '@app/vendor'. ## application -Приложение является центральным объектом на протяжении HTTP запроса. Оно сожержит несколько компонентов и с ними получает информацию из запроса и отправляет ее для дальнейшей обработки. +Приложение является центральным объектом на протяжении HTTP запроса. Оно содержит несколько компонентов и с ними +получает информацию из запроса и отправляет ее для дальнейшей обработки. -Объект приложения создается в виде синглтона входным скриптом. Объект приложения доступен из любого места через `\Yii::$app`. +Объект приложения создается в виде Singleton(шаблон проектирования Одиночка) входным скриптом. +Объект приложения доступен из любого места через `\Yii::$app`. ## assets @@ -34,19 +36,19 @@ Bundle, известный как пакет в Yii 1.1, относится к ## extension -Расширения это набор классов, комплект ресурсов и конфигураций, которые добавляют приложению функциональность. +Расширения - это набор классов, комплект ресурсов и конфигураций, которые добавляют приложению функциональность. # I ## installation -Установка это процесс подготовки чего-либо к работе либо путем чтения readme файла или выполнением подготовленного сценария. В случае Yii он устанавливает разрешения и необходимые зависимости. +Установка - это процесс подготовки чего-либо к работе либо путем чтения readme файла или выполнением подготовленного сценария. В случае Yii он устанавливает разрешения и необходимые зависимости. # M ## module -Модуль это подпрограмма которая содержит элементы MVC, такие как модели, представления, контроллеры и т.д. и может быть использована без главного приложения. Обычно пробрасывая запросы в модуль вместо обработки контроллером. +Модуль - это подпрограмма, которая содержит элементы MVC: модели, представления, контроллеры и т.д. Может быть использована без главного приложения. Обычно пробрасывая запросы в модуль вместо обработки контроллером. # N diff --git a/docs/guide-ru/helper-array.md b/docs/guide-ru/helper-array.md index 678575a..0397416 100644 --- a/docs/guide-ru/helper-array.md +++ b/docs/guide-ru/helper-array.md @@ -1,11 +1,11 @@ ArrayHelper =========== -Вдобавок к [богатому набору функций](https://secure.php.net/manual/ru/book.array.php) для работы с массивами, которые есть в самом PHP, хелпер Yii Array предоставляет свои статические функции, которые могут быть вам полезны. +Вдобавок к [богатому набору функций](https://secure.php.net/manual/ru/book.array.php) для работы с массивами, которые есть в самом PHP, хелпер Yii Array предоставляет свои статические функции - возможно они могут быть вам полезны. ## Получение значений -Извлечение значений из массива, объекта или структуры состоящей из них обоих с помощью стандартных средств PHP является довольно скучным занятием. Сперва вам нужно проверить есть ли соответствующий ключ с помощью `isset`, и если есть – получить, если нет – подставить значение по умолчанию. +Извлечение значений из массива, объекта или структуры состоящей из них обоих с помощью стандартных средств PHP является довольно скучным занятием. Сначала вам нужно проверить есть ли соответствующий ключ с помощью `isset`, и если есть – получить, если нет – подставить значение по умолчанию. ```php class User diff --git a/docs/guide-ru/helper-html.md b/docs/guide-ru/helper-html.md index 27aba40..5ffb14c 100644 --- a/docs/guide-ru/helper-html.md +++ b/docs/guide-ru/helper-html.md @@ -101,7 +101,7 @@ echo Html::tag('div', 'Сохранить', $options); // выведет '
Сохранить
' ``` -`Html::addCssClass()` предотвращает дублирование классов, поэтому можно не беспокоиться о том, что какой-либо класс +`Html::addCssClass()` предотвращает дублирование классов, поэтому можно не беспокоиться, что какой-либо класс будет добавлен дважды: ```php @@ -272,7 +272,7 @@ $decodedUserName = Html::decode($userName); ``` -Если же нет, используйте радио-переключатель: +Если нет, используйте радио-переключатель: ```php @@ -282,7 +282,7 @@ $decodedUserName = Html::decode($userName); ### Тэги label и отображение ошибок -Также как и для полей ввода, есть два метода формирования тэгов label для форм. Есть "active label", считывающий +Так же как и для полей ввода, есть два метода формирования тэгов label для форм. Есть "active label", считывающий данные из модели и обычный тэг "label", принимающий на вход непосредственно сами данные: ```php @@ -347,18 +347,18 @@ echo Html::getAttributeName('dates[0]'); Для формирования встроенных скриптов и стилей есть два метода: ```php - + 'print']) ?> Результатом будет: - + - true]) ?> + Результатом будет: - + ``` Если вы хотите подключить внешний CSS-файл: diff --git a/docs/guide-ru/helper-url.md b/docs/guide-ru/helper-url.md index 83df346..ac317fc 100644 --- a/docs/guide-ru/helper-url.md +++ b/docs/guide-ru/helper-url.md @@ -26,11 +26,11 @@ $absoluteBaseUrl = Url::base(true); $httpsAbsoluteBaseUrl = Url::base('https'); ``` -Единственный параметр данного метода работает также как и `Url::home()`. +Единственный параметр данного метода работает так же как и `Url::home()`. ## Создание URL -Чтобы создать URL для соответствующего роута используйте метод `Url::toRoute()`. Метод использует [[\yii\web\UrlManager]]. +Чтобы создать URL для соответствующего роута, используйте метод `Url::toRoute()`. Метод использует [[\yii\web\UrlManager]]. Для того чтобы создать URL: ```php @@ -62,7 +62,7 @@ $url = Url::toRoute(['product/view', 'id' => 42]); - Если роут начинается не со слеша (например, `site/index`), то он будет считаться относительным роутом текущего модуля и будет определен с помощью [[\yii\base\Module::uniqueId|uniqueId]]. -Начиная с версии 2.0.2, вы можете задавать роуты с помощью [псевдонимов](concept-aliases.md). В этом случае, сначала +Начиная с версии 2.0.2, вы можете задавать роуты с помощью [псевдонимов](concept-aliases.md). В этом случае сначала псевдоним будет сконвертирован в соответствующий роут, который будет преобразован в абсолютный в соответствии с вышеописанными правилами. diff --git a/docs/guide-ru/input-form-javascript.md b/docs/guide-ru/input-form-javascript.md index 7b0f635..cbc495d 100644 --- a/docs/guide-ru/input-form-javascript.md +++ b/docs/guide-ru/input-form-javascript.md @@ -105,7 +105,7 @@ function (event) ### `ajaxBeforeSend` -`ajaxBeforeSend` cобытие инициируется перед отправкой AJAX запроса для проверки основанной на AJAX. +`ajaxBeforeSend` событие инициируется перед отправкой AJAX запроса для проверки основанной на AJAX. Сигнатура обработчика события должна быть: diff --git a/docs/guide-ru/input-forms.md b/docs/guide-ru/input-forms.md index a388812..08e332e 100644 --- a/docs/guide-ru/input-forms.md +++ b/docs/guide-ru/input-forms.md @@ -56,9 +56,9 @@ $form = ActiveForm::begin([ В вышеприведённом коде [[yii\widgets\ActiveForm::begin()|ActiveForm::begin()]] не только создаёт экземпляр формы, но также и знаменует её начало. Весь контент, расположенный между [[yii\widgets\ActiveForm::begin()|ActiveForm::begin()]] и [[yii\widgets\ActiveForm::end()|ActiveForm::end()]], будет завёрнут в HTML-тег `
`. Вы можете изменить некоторые -настройки виджета через передачу массива в его `begin` метод, также как и в любом другом виджете. В этом случае дополнительный CSS-класс и идентификатор будет прикреплён к открывающемуся тегу ``. Для просмотра всех доступных настроек, пожалуйста, обратитесь к документации API [[yii\widgets\ActiveForm]]. +настройки виджета через передачу массива в его `begin` метод, так же как и в любом другом виджете. В этом случае дополнительный CSS-класс и идентификатор будет прикреплён к открывающемуся тегу ``. Для просмотра всех доступных настроек, пожалуйста, обратитесь к документации API [[yii\widgets\ActiveForm]]. -Для создания в форме элемента с меткой и любой применимой валидацией с помощью JavaScript, вызывается [[yii\widgets\ActiveForm::field()|ActiveForm::field()]], который возвращает экземпляр [[yii\widgets\ActiveField]]. Когда этот метод вызывается непосредственно, то результатом будет текстовый элемент (`input type="text"`). Для того, чтобы настроить элемент, можно вызвать один за одним дополнительные методы [[yii\widgets\ActiveField|ActiveField]]: +Для создания в форме элемента с меткой и любой применимой валидацией с помощью JavaScript, вызывается [[yii\widgets\ActiveForm::field()|ActiveForm::field()]], который возвращает экземпляр [[yii\widgets\ActiveField]]. Когда этот метод вызывается непосредственно, то результатом будет текстовый элемент (`input type="text"`). Для того, чтобы настроить элемент, можно вызвать один за другим дополнительные методы [[yii\widgets\ActiveField|ActiveField]]: ```php // элемент формы для ввода пароля @@ -94,7 +94,7 @@ echo $form->field($model, 'items[]')->checkboxList(['a' => 'Item A', 'b' => 'Ite Дополнительные HTML-элементы можно добавить к форме, используя обычный HTML или методы из класса помощника [[yii\helpers\Html|Html]], как это было сделано с помощью [[yii\helpers\Html::submitButton()|Html::submitButton()]] в примере, приведённом выше. -> Tip: Если вы использует Twitter Bootstrap CSS в своём приложении, то воспользуйтесь [[yii\bootstrap\ActiveForm]] вместо [[yii\widgets\ActiveForm]]. Он добавит к ActiveForm дополнительные стили, которые сработают в рамках bootstrap CSS. +> Tip: Если вы используете Twitter Bootstrap CSS в своём приложении, то воспользуйтесь [[yii\bootstrap\ActiveForm]] вместо [[yii\widgets\ActiveForm]]. Он добавит к ActiveForm дополнительные стили, которые сработают в рамках bootstrap CSS. > Tip: для добавления "звёздочки" к обязательным элементам формы, воспользуйтесь следующим CSS: > @@ -153,7 +153,7 @@ Pjax::begin([ ActiveForm::end(); Pjax::end(); ``` -> Tip: Будьте осторожны с ссылками внутри виджета [[yii\widgets\Pjax|Pjax]] так как ответ будет +> Tip: Будьте осторожны со ссылками внутри виджета [[yii\widgets\Pjax|Pjax]] так как ответ будет > также отображаться внутри виджета. Чтобы ссылка работала без PJAX, добавьте к ней HTML-атрибут > `data-pjax="0"`. diff --git a/docs/guide-ru/intro-upgrade-from-v1.md b/docs/guide-ru/intro-upgrade-from-v1.md index 7bef066..af2239a 100644 --- a/docs/guide-ru/intro-upgrade-from-v1.md +++ b/docs/guide-ru/intro-upgrade-from-v1.md @@ -8,7 +8,7 @@ Если прежде вы не использовали Yii 1.1, вы можете сразу перейти к разделу «[Начало работы](start-installation.md)». Также учтите, что в Yii 2.0 гораздо больше новых возможностей, чем описано далее. Настоятельно рекомендуется, изучить -всё руководство. Вполне возможно, что то, что раньше приходилось разрабатывать самостоятельно теперь является частью +всё руководство. Вполне возможно, что-то, что раньше приходилось разрабатывать самостоятельно теперь является частью фреймворка. @@ -16,7 +16,7 @@ -------- Yii 2.0 широко использует [Composer](https://getcomposer.org/), который является основным менеджером зависимостей для PHP. -Установка как фреймворка, так и расширений, осуществляется через Composer. Подробно о установке Yii 2.0 вы можете узнать +Установка как фреймворка, так и расширений, осуществляется через Composer. Подробно об установке Yii 2.0 вы можете узнать из раздела «[Установка Yii](start-installation.md)». О том, как создавать расширения для Yii 2.0 или адаптировать уже имеющиеся расширения от версии 1.1, вы можете узнать из раздела «[Создание расширений](structure-extensions.md#creating-extensions)». @@ -260,7 +260,7 @@ ActiveForm::end(); В Yii 2.0 темы работают совершенно по-другому. Теперь они основаны на механизме сопоставления путей исходного файла представления с темизированным файлом. Например, если используется сопоставление путей `['/web/views' => '/web/themes/basic']`, -то темизированная версия файла представления `/web/views/site/index.php` будет находится в `/web/themes/basic/site/index.php`. +то темизированная версия файла представления `/web/views/site/index.php` будет находиться в `/web/themes/basic/site/index.php`. По этой причине темы могут быть применены к любому файлу представления, даже к представлению, отрендеренному внутри контекста контроллера или виджета. Также, больше не существует компонента `CThemeManager`. Вместо этого, `theme` является конфигурируемым свойством компонента приложения `view`. @@ -399,7 +399,7 @@ Active Record построение запросов и работу со связями. Класс `CDbCriteria` версии 1.1 был заменен [[yii\db\ActiveQuery]]. Этот класс наследуется от [[yii\db\Query]] и таким -образом получает все методы, необходимые для построения запроса. Чтобы начать строить запрос следует вызвать метод +образом получает все методы, необходимые для построения запроса. Чтобы начать строить запрос, следует вызвать метод [[yii\db\ActiveRecord::find()]]: ```php diff --git a/docs/guide-ru/intro-yii.md b/docs/guide-ru/intro-yii.md index 10d68c8..9bed6ab 100644 --- a/docs/guide-ru/intro-yii.md +++ b/docs/guide-ru/intro-yii.md @@ -42,7 +42,7 @@ Yii — не проект одного человека. Он поддержив Требования к ПО и знаниям ------------------------- -Yii 2.0 требует PHP 5.4.0 и выше и наилучшим образом работает на последней версии PHP 7. Чтобы узнать требования для отдельных возможностей вы можете запустить скрипт проверки +Yii 2.0 требует PHP 5.4.0 и выше и наилучшим образом работает на последней версии PHP 7. Чтобы узнать требования для отдельных возможностей, вы можете запустить скрипт проверки требований, который поставляется с каждым релизом фреймворка. Для разработки на Yii потребуется общее понимание ООП, так как фреймворк полностью следует этой парадигме. Также стоит diff --git a/docs/guide-ru/output-data-providers.md b/docs/guide-ru/output-data-providers.md index 8add3e6..4ce4c12 100644 --- a/docs/guide-ru/output-data-providers.md +++ b/docs/guide-ru/output-data-providers.md @@ -170,8 +170,8 @@ $provider = new ArrayDataProvider([ $rows = $provider->getModels(); ``` -> Note: Сравнивая с [Active Data Provider](#active-data-provider) и [SQL Data Provider](#sql-data-provider), -ArrayDataProvider менее эффективный потому, что требует загрузки *всех* данных в память. +> Note: По сравнению с [Active Data Provider](#active-data-provider) и [SQL Data Provider](#sql-data-provider), +ArrayDataProvider менее эффективный, потому что требует загрузки *всех* данных в память. ## Принципы работы с ключами данных diff --git a/docs/guide-ru/rest-error-handling.md b/docs/guide-ru/rest-error-handling.md index 73106f2..ab9570a 100644 --- a/docs/guide-ru/rest-error-handling.md +++ b/docs/guide-ru/rest-error-handling.md @@ -3,7 +3,7 @@ Если при обработке запроса к RESTful API в запросе пользователя обнаруживается ошибка или происходит что-то непредвиденное на сервере, вы можете просто выбрасывать исключение, чтобы уведомить пользователя о нештатной ситуации. -Если же вы можете установить конкретную причину ошибки (например, запрошенный ресурс не существует), вам следует подумать +Если вы можете установить конкретную причину ошибки (например, запрошенный ресурс не существует), вам следует подумать о том, чтобы выбрасывать исключение с соответствующим кодом состояния HTTP (например, [[yii\web\NotFoundHttpException]], соответствующее коду состояния 404). Yii отправит ответ с соответствующим HTTP-кодом и текстом. Он также включит в тело ответа сериализованное представление @@ -89,4 +89,4 @@ return [ ``` Приведённый выше код изменит формат ответа (как для удачного запроса, так и для ошибок) если передан `GET`-параметр -`suppress_response_code`. \ No newline at end of file +`suppress_response_code`. diff --git a/docs/guide-ru/rest-resources.md b/docs/guide-ru/rest-resources.md index 982a40b..5d530a6 100644 --- a/docs/guide-ru/rest-resources.md +++ b/docs/guide-ru/rest-resources.md @@ -14,7 +14,7 @@ RESTful API строятся вокруг доступа к *ресурсам* В этом разделе, мы сосредоточимся на том, как при помощи класса ресурса, наследуемого от [[yii\base\Model]] (или дочерних классов) задать какие данные будут возвращаться RESTful API. Если класс ресурса не наследуется от -[[yii\base\Model]], возвращаются всего его public свойства. +[[yii\base\Model]], возвращаются все его public свойства. ## Поля @@ -22,7 +22,7 @@ RESTful API строятся вокруг доступа к *ресурсам* Когда ресурс включается в ответ RESTful API, необходимо сериализовать его в строку. Yii разбивает этот процесс на два этапа. Сначала ресурс конвертируется в массив при помощи [[yii\rest\Serializer]]. На втором этапе массив сериализуется в строку заданного формата (например, JSON или XML) при помощи [[yii\web\ResponseFormatterInterface|форматтера ответа]]. -Именно на этом стоит сосредоточится при разработке класса ресурса. +Именно на этом стоит сосредоточиться при разработке класса ресурса. Вы можете указать какие данные включать в представление ресурса в виде массива путём переопределения методов [[yii\base\Model::fields()|fields()]] и/или [[yii\base\Model::extraFields()|extraFields()]]. Разница между ними в том, diff --git a/docs/guide-ru/rest-response-formatting.md b/docs/guide-ru/rest-response-formatting.md index f9e5322..c8fdbd7 100644 --- a/docs/guide-ru/rest-response-formatting.md +++ b/docs/guide-ru/rest-response-formatting.md @@ -164,7 +164,7 @@ Content-Type: application/json; charset=UTF-8 может пригодиться для более тонкой настройки кодирования. Свойство [[yii\web\Response::formatters|formatters]] компонента приложения `response` может быть -[сконфигурировано](concept-configuration.md) следующим образом: +[сконфигурировано](concept-configurations.md) следующим образом: ```php 'response' => [ diff --git a/docs/guide-ru/rest-routing.md b/docs/guide-ru/rest-routing.md index 137dafa..9f1478a 100644 --- a/docs/guide-ru/rest-routing.md +++ b/docs/guide-ru/rest-routing.md @@ -50,7 +50,7 @@ * `OPTIONS /users/123`: список HTTP-методов, поддерживаемые точкой входа `/users/123`. Вы можете настроить опции `only` и `except`, явно указав для них список действий, которые поддерживаются или -которые должны быть отключены, соответственно. Например: + должны быть отключены, соответственно. Например: ```php [ diff --git a/docs/guide-ru/runtime-logging.md b/docs/guide-ru/runtime-logging.md index 24f693e..cc42a8c 100644 --- a/docs/guide-ru/runtime-logging.md +++ b/docs/guide-ru/runtime-logging.md @@ -17,7 +17,7 @@ Yii предоставляет мощную, гибко настраиваему * [[Yii::debug()]]: записывает сообщения для отслеживания выполнения кода приложения. Используется, в основном, при разработке. * [[Yii::info()]]: записывает сообщение, содержащее какую-либо полезную информацию. * [[Yii::warning()]]: записывает *тревожное* сообщение при возникновении неожиданного события. -* [[Yii::error()]]: записывает критическую ошибку, на которую нужно, как можно скорее, обратить внимаение. +* [[Yii::error()]]: записывает критическую ошибку, на которую нужно, как можно скорее, обратить внимание. Эти методы позволяют записывать сообщения разных *уровней важности* и *категорий*. Они имеют одинаковое описание функции `function ($message, $category = 'application')`, где `$message` передает сообщение для записи, а `$category` - категорию сообщения. В следующем примере будет записано *trace* сообщение с категорией по умолчанию `application`: @@ -222,7 +222,7 @@ return [ ] ``` -Из-за того, что значения максимального количества сообщений для передачи и выгрузки по умолчанию достаточно велико, при вызове метода `Yii::debug()` или любого другого метода логирования, сообщение не появится сразу в файле или таблице базы данных. Такое поведение может стать проблемой, например, в консольных приложениях с большим временем исполнения. Для того, чтобы все сообщения логов сразу же попадали в лог, необходимо установить значения свойств [[yii\log\Dispatcher::flushInterval|flushInterval]] и [[yii\log\Target::exportInterval|exportInterval]] равными 1, например так: +Из-за того, что значения максимального количества сообщений для передачи и выгрузки по умолчанию достаточно велико, при вызове метода `Yii::debug()` или любого другого метода логирования, сообщение не появится сразу в файле или таблице базы данных. Такое поведение может стать проблемой, например, в консольных приложениях с большим временем исполнения. Для того, чтобы все сообщения логов сразу попадали в лог, необходимо установить значения свойств [[yii\log\Dispatcher::flushInterval|flushInterval]] и [[yii\log\Target::exportInterval|exportInterval]] равными 1, например так: ```php return [ diff --git a/docs/guide-ru/runtime-requests.md b/docs/guide-ru/runtime-requests.md index 0ecf158..3ba36bf 100644 --- a/docs/guide-ru/runtime-requests.md +++ b/docs/guide-ru/runtime-requests.md @@ -45,7 +45,7 @@ $params = $request->bodyParams; $param = $request->getBodyParam('id'); ``` -> Info: В отличии от `GET` параметров, параметры, которые были переданы через `POST`, `PUT`, `PATCH` и д.р. отправляются в теле запроса. +> Info: В отличие от `GET` параметров, параметры, которые были переданы через `POST`, `PUT`, `PATCH` и д.р. отправляются в теле запроса. Компонент `request` будет обрабатывать эти параметры, когда вы попробуете к ним обратиться через методы, описанные выше. Вы можете настроить способ обработки этих параметров через настройку свойства [[yii\web\Request::parsers]]. @@ -75,7 +75,7 @@ if ($request->isPut) { /* текущий запрос является PUT за * [[yii\web\Request::absoluteUrl|absoluteUrl]]: вернёт адрес `http://example.com/admin/index.php/product?id=100`, который содержит полный URL, включая имя хоста. * [[yii\web\Request::hostInfo|hostInfo]]: вернёт адрес `http://example.com`, который содержит только имя хоста. -* [[yii\web\Request::pathInfo|pathInfo]]: вернёт адрес `/product`, который содержит часть между адресом начального скрипта и параметрами запроса, которые идут после знака вопроса. +* [[yii\web\Request::pathInfo|pathInfo]]: вернёт адрес `/product`, который содержит часть между адресом начального скрипта и параметрами запроса, идущих после знака вопроса. * [[yii\web\Request::queryString|queryString]]: вернёт адрес `id=100`, который содержит часть URL после знака вопроса. * [[yii\web\Request::baseUrl|baseUrl]]: вернёт адрес `/admin`, который является частью URL после информации о хосте и перед именем входного скрипта. * [[yii\web\Request::scriptUrl|scriptUrl]]: вернёт адрес `/admin/index.php`, который содержит URL без информации о хосте и параметрах запроса. @@ -138,7 +138,7 @@ $userIP = Yii::$app->request->userIP; [[yii\web\Request::ipHeaders|ipHeaders]] и [[yii\web\Request::secureProtocolHeaders|secureProtocolHeaders]] -Ниже приведена конфигурация компонента `request` для приложения, которое работает за рядом обратных прокси, которые расположены в IP-сети `10.0.2.0/24`: +Ниже приведена конфигурация компонента `request` для приложения, которое работает за рядом обратных прокси, расположенных в IP-сети `10.0.2.0/24`: ```php 'request' => [ @@ -176,4 +176,4 @@ IP-адрес, по умолчанию, отправляется прокси-с ], ``` В приведенной выше конфигурации все заголовки, перечисленные в `secureHeaders`, отфильтровываются из запроса, кроме заголовков `X-ProxyUser-Ip` и `Front-End-Https` в случае, если запрос создан прокси. -В этом случае, первый используется для получения IP-адреса пользователя, настроенного в `ipHeaders`, а последний будет использоваться для определения результата [[yii\web\Request::getIsSecureConnection()]]. \ No newline at end of file +В этом случае первый используется для получения IP-адреса пользователя, настроенного в `ipHeaders`, а последний будет использоваться для определения результата [[yii\web\Request::getIsSecureConnection()]]. diff --git a/docs/guide-ru/runtime-responses.md b/docs/guide-ru/runtime-responses.md index af63803..b06b709 100644 --- a/docs/guide-ru/runtime-responses.md +++ b/docs/guide-ru/runtime-responses.md @@ -24,7 +24,7 @@ Yii::$app->response->statusCode = 200; ``` Однако в большинстве случаев явная установка не требуется так как значение [[yii\web\Response::statusCode]] -по умолчанию равно 200. Если же вам нужно показать, что запрос не удался, вы можете выбросить соответствующее +по умолчанию равно 200. Если вам нужно показать, что запрос не удался, вы можете выбросить соответствующее HTTP-исключение: ```php @@ -216,8 +216,8 @@ public function actionDownload() } ``` -При вызове метода отправки файла вне методов действий чтобы быть уверенным, что к ответу не будет добавлено никакое -нежелательное содержимое, следует вызвать сразу после него [[yii\web\Response::send()]]. +Чтобы быть уверенным, что к ответу не будет добавлено никакое +нежелательное содержимое, при вызове метода [[yii\web\Response::sendFile()]] вне методов action, следует вызвать сразу после него [[yii\web\Response::send()]]. ```php \Yii::$app->response->sendFile('path/to/file.txt')->send(); @@ -239,7 +239,7 @@ Web-серверов: ## Отправка ответа Содержимое ответа не отправляется пользователю до вызова метода [[yii\web\Response::send()]]. По умолчанию он вызывается -автоматически в конце метода [[yii\base\Application::run()]]. Однако, чтобы ответ был отправлен немедленно, вы можете +автоматически в конце метода [[yii\base\Application::run()]]. Однако чтобы ответ был отправлен немедленно, вы можете вызвать этот метод явно. Для отправки ответа метод [[yii\web\Response::send()]] выполняет следующие шаги: diff --git a/docs/guide-ru/runtime-routing.md b/docs/guide-ru/runtime-routing.md index 4a419b6..578e868 100644 --- a/docs/guide-ru/runtime-routing.md +++ b/docs/guide-ru/runtime-routing.md @@ -453,8 +453,8 @@ echo Url::previous(); ] ``` -> Note: по умолчанию [[yii\web\UrlManager::$normalizer|UrlManager::$normalizer]] отключен. Чтобы использовать - нормализацию его необходимо сконфигурировать. +> Note: По умолчанию [[yii\web\UrlManager::$normalizer|UrlManager::$normalizer]] отключен. Чтобы использовать + нормализацию, его необходимо сконфигурировать. ### Методы HTTP diff --git a/docs/guide-ru/security-authorization.md b/docs/guide-ru/security-authorization.md index 15e8013..d32d72e 100644 --- a/docs/guide-ru/security-authorization.md +++ b/docs/guide-ru/security-authorization.md @@ -224,6 +224,10 @@ return [ ]; ``` +> Примечание: Если вы используете шаблон проекта basic, компонент `authManager` необходимо настроить как в `config/web.php`, так и в +> [конфигурации консольного приложения](tutorial-console.md#configuration) `config/console.php`. +> При использовании шаблона проекта advanced `authManager` достаточно настроить единожды в `common/config/main.php`. + `DbManager` использует четыре таблицы для хранения данных: - [[yii\rbac\DbManager::$itemTable|itemTable]]: таблица для хранения авторизационных элементов. По умолчанию "auth_item". @@ -539,7 +543,7 @@ $auth->addChild($admin, $author); // ... add permissions as children of $admin ... ``` -Обратите внимание, так как "author" добавлен как дочерняя роль к "admin", следовательно в реализации метода `execute()` +Обратите внимание, так как "author" добавлен как дочерняя роль к "admin", следовательно, в реализации метода `execute()` класса правила вы должны учитывать эту иерархию. Именно поэтому для роли "author" метод `execute()` вернёт истину, если пользователь принадлежит к группам 1 или 2 (это означает, что пользователь находится в группе администраторов или авторов) diff --git a/docs/guide-ru/start-forms.md b/docs/guide-ru/start-forms.md index 1401f8a..73a7660 100644 --- a/docs/guide-ru/start-forms.md +++ b/docs/guide-ru/start-forms.md @@ -18,7 +18,7 @@ --------------------------------------------- В файле `models/EntryForm.php` создайте класс модели `EntryForm` как показано ниже. Он будет использоваться для -хранения данных, введённых пользователем. Подробно о именовании файлов классов читайте в разделе +хранения данных, введённых пользователем. Подробно об именовании файлов классов читайте в разделе «[Автозагрузка классов](concept-autoloading.md)». ```php diff --git a/docs/guide-ru/start-hello.md b/docs/guide-ru/start-hello.md index d07aa57..e4398e7 100644 --- a/docs/guide-ru/start-hello.md +++ b/docs/guide-ru/start-hello.md @@ -109,10 +109,10 @@ http://hostname/index.php?r=site%2Fsay&message=Привет+мир Параметр `r` в нашем URL требует дополнительных пояснений. Он связан с [маршрутом (route)](runtime-routing.md), который представляет собой уникальный идентификатор, указывающий на действие. Его формат `ControllerID/ActionID`. Когда приложение получает -запрос, оно проверяет параметр `r` и, используя `ControllerID`, определяет какой контроллер следует использовать для -обработки запроса. Затем, контроллер использует часть `ActionID`, чтобы определить какое действие выполняет реальную работу. +запрос, оно проверяет параметр `r` и, используя `ControllerID`, определяет какой контроллер использовать для +обработки запроса. Затем, контроллер использует часть `ActionID`, чтобы определить, какое действие выполняет реальную работу. В нашем случае маршрут `site/say` будет соответствовать контроллеру `SiteController` и его действию `say`. -В результате, для обработки запроса будет вызван метод `SiteController::actionSay()`. +В результате для обработки запроса будет вызван метод `SiteController::actionSay()`. > Info: Как и действия, контроллеры также имеют идентификаторы, которые однозначно определяют их в приложении. Идентификаторы контроллеров используют те же правила именования, что и идентификаторы действий. Имена классов diff --git a/docs/guide-ru/start-workflow.md b/docs/guide-ru/start-workflow.md index ca0847e..2357de2 100644 --- a/docs/guide-ru/start-workflow.md +++ b/docs/guide-ru/start-workflow.md @@ -76,7 +76,7 @@ basic/ корневой каталог приложения 1. Пользователь обращается к [точке входа](structure-entry-scripts.md) `web/index.php`. 2. Скрипт загружает конфигурацию [configuration](concept-configurations.md) и создает экземпляр [приложения](structure-applications.md) для дальнейшей обработки запроса. -3. Приложение определяет [маршрут](runtime-routing.md) запроса при помощи компонента приложения [запрос](runtime-requests.md). +3. Приложение определяет [маршрут](runtime-routing.md) запроса при помощи компонента приложения [запрос](runtime-requests.md). 4. Приложение создает экземпляр [контроллера](structure-controllers.md) для выполнения запроса. 5. Контроллер, в свою очередь, создает [действие](structure-controllers.md) и накладывает на него фильтры. 6. Если хотя бы один фильтр дает сбой, выполнение приложения останавливается. diff --git a/docs/guide-ru/structure-applications.md b/docs/guide-ru/structure-applications.md index 7425784..df5238d 100644 --- a/docs/guide-ru/structure-applications.md +++ b/docs/guide-ru/structure-applications.md @@ -56,7 +56,7 @@ $config = require __DIR__ . '/../config/web.php'; #### [[yii\base\Application::basePath|basePath]] Свойство [[yii\base\Application::basePath|basePath]] указывает на корневую директорию приложения. Эта директория содержит -весь защищенный исходный код приложения. В данной директории обычно могут находится поддиректории `models`, `views`, +весь защищенный исходный код приложения. В данной директории обычно могут находиться поддиректории `models`, `views`, `controllers`, содержащие код, соответствующий шаблону проектирования MVC. Вы можете задать свойство [[yii\base\Application::basePath|basePath]] используя путь к директории или используя @@ -71,7 +71,7 @@ $config = require __DIR__ . '/../config/web.php'; ### Важные свойства -Свойства, указанные в этом подразделе, часто нуждаются в преднастройке т.к. они разнятся от приложения к приложению. +Свойства, указанные в этом подразделе, часто нуждаются в предварительной настройке т.к. они разнятся от приложения к приложению. #### [[yii\base\Application::aliases|aliases]] @@ -373,7 +373,7 @@ $width = \Yii::$app->params['thumbnail.size'][0]; #### [[yii\base\Application::charset|charset]] -Свойство указывает кодировку, которую использует приложение. По-умолчанию значение равно `'UTF-8'`, которое должно быть +Свойство указывает кодировку, которую использует приложение. По-умолчанию значение равно `'UTF-8'` - должно быть оставлено как есть для большинства приложения, только если вы не работаете с устаревшим кодом, который использует большее количество данных не юникода. @@ -509,7 +509,7 @@ $width = \Yii::$app->params['thumbnail.size'][0]; ### [[yii\base\Application::EVENT_BEFORE_REQUEST|EVENT_BEFORE_REQUEST]] -Данное событие возникает *до* того как приложение начинает обрабатывать входящий запрос. +Данное событие возникает *до* того, как приложение начинает обрабатывать входящий запрос. Настоящее имя события - `beforeRequest`. На момент возникновения данного события, объект приложения уже создан и проинициализирован. Таким образом, это @@ -520,7 +520,7 @@ $width = \Yii::$app->params['thumbnail.size'][0]; ### [[yii\base\Application::EVENT_AFTER_REQUEST|EVENT_AFTER_REQUEST]] -Данное событие возникает *после* того как приложение заканчивает обработку запроса, но *до* того как произойдет +Данное событие возникает *после* того, как приложение заканчивает обработку запроса, но *до* того как произойдет отправка ответа. Настоящее имя события - `afterRequest`. На момент возникновения данного события, обработка запроса завершена и вы можете воспользоваться этим для произведения постобработки @@ -532,7 +532,7 @@ $width = \Yii::$app->params['thumbnail.size'][0]; ### [[yii\base\Application::EVENT_BEFORE_ACTION|EVENT_BEFORE_ACTION]] -Событие возникает *до* того как будет выполнено [действие контроллера](structure-controllers.md). +Событие возникает *до* того, как будет выполнено [действие контроллера](structure-controllers.md). Настоящее имя события - `beforeAction`. Событие является объектом [[yii\base\ActionEvent]]. Обработчик события может устанавливать @@ -571,7 +571,7 @@ $width = \Yii::$app->params['thumbnail.size'][0]; [ 'on afterAction' => function ($event) { if (некоторое условие) { - // modify $event->result + // обрабатываем $event->result } else { } }, diff --git a/docs/guide-ru/structure-assets.md b/docs/guide-ru/structure-assets.md index e46e2c4..aa4c412 100644 --- a/docs/guide-ru/structure-assets.md +++ b/docs/guide-ru/structure-assets.md @@ -124,12 +124,38 @@ class FontAwesomeAsset extends AssetBundle public $css = [ 'css/font-awesome.min.css', ]; + public $publishOptions = [ + 'only' => [ + 'fonts/*', + 'css/*', + ] + ]; +} +``` + +Более сложную логику можно реализовать с помощью переопределения `init()`. Ниже указан пример публикации поддиректорий этим способом: + +```php +publishOptions['beforeCopy'] = function ($from, $to) { - $dirname = basename(dirname($from)); + if (basename(dirname($from)) !== 'font-awesome') { + return true; + } + $dirname = basename($from); return $dirname === 'fonts' || $dirname === 'css'; }; } diff --git a/docs/guide-ru/structure-controllers.md b/docs/guide-ru/structure-controllers.md index 9fe1fee..59bf52e 100644 --- a/docs/guide-ru/structure-controllers.md +++ b/docs/guide-ru/structure-controllers.md @@ -188,7 +188,7 @@ ID контроллеров также могут содержать префи ## Создание действий -Создание действий не представляет сложностей также как и объявление так называемых *методов действий* в классе контроллера. Метод действия +Создание действий не представляет сложностей так же как и объявление так называемых *методов действий* в классе контроллера. Метод действия это *public* метод, имя которого начинается со слова `action`. Возвращаемое значение метода действия представляет собой ответные данные, которые будут высланы конечному пользователю. Приведенный ниже код определяет два действия `index` и `hello-world`: @@ -240,7 +240,7 @@ class SiteController extends Controller Например, `index` соответствует `actionIndex`, а `hello-world` соответствует `actionHelloWorld`. > Note: Названия имен действий являются *регистрозависимыми*. Если у вас есть метод `ActionIndex`, он не будет - учтен как метод действия, таким образом, запрос к действию `index` приведет к выбросу исключению. Также следует учесть, что методы действий + учтен как метод действия, таким образом, запрос к действию `index` приведет к выбросу исключения. Также следует учесть, что методы действий должны иметь область видимости public. Методы имеющие область видимости private или protected НЕ определяют методы встроенных действий. @@ -305,7 +305,7 @@ class HelloWorldAction extends Action в качестве ответа. * Для [[yii\web\Application|Веб приложений]], возвращаемое значение также может быть произвольными данными, которые будут - присвоены [[yii\web\Response::data]], а затем сконвертированы в строку, представляющую тело ответа. + присвоены [[yii\web\Response::data]], а затем конвертированы в строку, представляющую тело ответа. * Для [[yii\console\Application|Консольных приложений]], возвращаемое значение также может быть числом, представляющим [[yii\console\Response::exitStatus|статус выхода]] исполнения команды. @@ -367,7 +367,7 @@ public function actionView(array $id, $version = null) Теперь, если запрос будет содержать URL `http://hostname/index.php?r=post/view&id[]=123`, то параметр `$id` примет значение `['123']`. Если запрос будет содержать URL `http://hostname/index.php?r=post/view&id=123`, то параметр `$id` все равно будет -содержать массив, т. к. скалярное значение `'123'` будет автоматически сконвертировано в массив. +содержать массив, т. к. скалярное значение `'123'` будет автоматически конвертировано в массив. Вышеприведенные примеры в основном показывают как параметры действий работают для Веб приложений. Больше информации о параметрах консольных приложений представлено в секции [Консольные команды](tutorial-console.md). diff --git a/docs/guide-ru/structure-models.md b/docs/guide-ru/structure-models.md index e5e63e2..b2452dc 100644 --- a/docs/guide-ru/structure-models.md +++ b/docs/guide-ru/structure-models.md @@ -143,7 +143,7 @@ $model = new User(['scenario' => User::SCENARIO_LOGIN]); ``` По умолчанию сценарии, поддерживаемые моделью, определяются [правилами валидации](#validation-rules) объявленными -в модели. Однако, Вы можете изменить это поведение путем переопределения метода [[yii\base\Model::scenarios()]] как показано ниже: +в модели. Однако Вы можете изменить это поведение путем переопределения метода [[yii\base\Model::scenarios()]] как показано ниже: ```php namespace app\models; @@ -169,7 +169,7 @@ class User extends ActiveRecord Метод `scenarios()` возвращает массив, ключами которого являются имена сценариев, а значения - соответствующие *активные атрибуты*. Активные атрибуты могут быть [массово присвоены](#massive-assignment) и подлежат [валидации](#validation-rules). В приведенном выше примере, атрибуты `username` и `password` это активные атрибуты сценария `login`, а в сценарии `register` так же активным атрибутом является `email` вместе с `username` и `password`. -По умолчанию реализация `scenarios()` вернёт все найденные сценарии в правилах валидации задекларированных в методе [[yii\base\Model::rules()]]. При переопределении метода `scenarios()`, если Вы хотите ввести новые сценарии помимо стандартных, Вы можете написать код на основе следующего примера: +По умолчанию реализация `scenarios()` вернёт все найденные сценарии в правилах валидации, задекларированных в методе [[yii\base\Model::rules()]]. При переопределении метода `scenarios()`, если Вы хотите ввести новые сценарии помимо стандартных, Вы можете написать код на основе следующего примера: ```php namespace app\models; @@ -326,14 +326,14 @@ $model->secret = $secret; Часто нужно экспортировать модели в различные форматы. Например, может потребоваться преобразовать коллекцию моделей в JSON или Excel формат. Процесс экспорта может быть разбит на два самостоятельных шага. На первом этапе модели преобразуются в массивы; на втором этапе массивы преобразуются в целевые форматы. Вы можете сосредоточиться только на первом шаге потому, что второй шаг может быть достигнут путем универсального инструмента форматирования данных, такого как [[yii\web\JsonResponseFormatter]]. Самый простой способ преобразования модели в массив - использовать свойство [[yii\base\Model::$attributes]]. -Например, +Например ```php $post = \app\models\Post::findOne(100); $array = $post->attributes; ``` -По умолчанию, свойство [[yii\base\Model::$attributes]] возвращает значения *всех* атрибутов объявленных в [[yii\base\Model::attributes()]]. +По умолчанию свойство [[yii\base\Model::$attributes]] возвращает значения *всех* атрибутов объявленных в [[yii\base\Model::attributes()]]. Более гибкий и мощный способ конвертирования модели в массив - использовать метод [[yii\base\Model::toArray()]]. Его поведение по умолчанию такое же как и у [[yii\base\Model::$attributes]]. Тем не менее, он позволяет выбрать, какие элементы данных, называемые *полями*, поставить в результирующий массив и как они должны быть отформатированы. На самом деле, этот способ экспорта моделей по умолчанию применяется при разработке в RESTful Web service, как описано в [Response Formatting](rest-response-formatting.md). diff --git a/docs/guide-ru/structure-modules.md b/docs/guide-ru/structure-modules.md index ec9d574..8b1fce1 100644 --- a/docs/guide-ru/structure-modules.md +++ b/docs/guide-ru/structure-modules.md @@ -1,7 +1,7 @@ Модули ======= -Модули - это законченные программные блоки, состоящие из [моделей](structure-models.md), [представлений](structure-views.md), [контроллеров](structure-controllers.md) и других вспомогательных компонентов. При установке модулей в [приложение](structure-applications.md), конечный пользователь получает доступ к их контроллерам. По этой причине модули часто рассматриваются как миниатюрные приложения. В отличии от [приложений](structure-applications.md), модули нельзя развертывать отдельно. Модули должны находиться внутри приложений. +Модули - это законченные программные блоки, состоящие из [моделей](structure-models.md), [представлений](structure-views.md), [контроллеров](structure-controllers.md) и других вспомогательных компонентов. При установке модулей в [приложение](structure-applications.md), конечный пользователь получает доступ к их контроллерам. По этой причине модули часто рассматриваются как миниатюрные приложения. В отличие от [приложений](structure-applications.md), модули нельзя развертывать отдельно. Модули должны находиться внутри приложений. ## Создание модулей diff --git a/docs/guide-ru/test-environment-setup.md b/docs/guide-ru/test-environment-setup.md index bc53747..6c1a35f 100644 --- a/docs/guide-ru/test-environment-setup.md +++ b/docs/guide-ru/test-environment-setup.md @@ -14,7 +14,7 @@ Yii 2 официально поддерживает интеграцию с фр [`yii2-basic`](https://github.com/yiisoft/yii2-app-basic) и [`yii2-advanced`](https://github.com/yiisoft/yii2-app-advanced). -Для того, чтобы запустить тесты необходимо установить [Codeception](https://github.com/Codeception/Codeception). +Для того, чтобы запустить тесты, необходимо установить [Codeception](https://github.com/Codeception/Codeception). Сделать это можно как локально, то есть только для текущего проекта, так и глобально для компьютера разработчика. Для локальной установки используйте следующие команды: @@ -53,7 +53,7 @@ Changed current directory to Если вы используете Apache и настроили его как описано в разделе «[Установка Yii](start-installation.md)», то для тестов вам необходимо создать отдельный виртуальный хост который будет работать с той же папкой, но использовать входной скрипт `index-test.php`: ``` - DocumentRoot "path/to/basic/webb" + DocumentRoot "path/to/basic/web" ServerName mysite-test Order Allow,Deny diff --git a/docs/guide-ru/test-fixtures.md b/docs/guide-ru/test-fixtures.md index 01e32ff..56a054c 100644 --- a/docs/guide-ru/test-fixtures.md +++ b/docs/guide-ru/test-fixtures.md @@ -11,7 +11,7 @@ загружаете один или несколько объектов фикстур перед запуском теста и выгружаете их после его завершения. Фикстура может зависеть от других фикстур, заданных через свойство [[yii\test\Fixture::depends]]. -Когда фикстура загружается, фикстуры от которых она зависит будут автоматически загружены ДО нее, а когда она +Когда фикстура загружается, фикстуры, от которых она зависит, будут автоматически загружены ДО нее, а когда она выгружается все зависимые фикстуры будут выгружены ПОСЛЕ нее. @@ -49,7 +49,7 @@ class UserFixture extends ActiveFixture Данные для фикстуры `ActiveFixture`, как правило, находятся в файле `FixturePath/data/TableName.php`, где `FixturePath` указывает на директорию, в которой располагается файл класса фикстуры, а `TableName` на имя таблицы, -с которой она ассоциируется. Для примера выше, данные должны должны быть в файле `@app/tests/fixtures/data/user.php`. +с которой она ассоциируется. Для примера выше, данные должны быть в файле `@app/tests/fixtures/data/user.php`. Данный файл должен вернуть массив данных для строк, которые будут вставлены в таблицу пользователей. Например ```php @@ -295,7 +295,7 @@ return [ Класс фикстур должны содержать суффикс `Fixture`. По умолчанию поиск фикстур выполняется в пространстве имен `tests\unit\fixtures`, но вы можете изменить это поведение через конфигурационный файл или параметры команды. Вы можете исключить некоторые фикстуры из загрузки или выгрузки добавив `-` перед их именем, например `-User`. -Чтобы загрузить фикстуру, выполните следующую команду:: +Чтобы загрузить фикстуру, выполните следующую команду: ``` yii fixture/load @@ -308,7 +308,7 @@ yii fixture/load // загрузить фикстуру `User` yii fixture/load User -// то же что и выше, т.к. "load" является действие по умолчанию для команды "fixture" +// то же что и выше, т.к. "load" является действием по умолчанию для команды "fixture" yii fixture User // загрузить нескольких фикстур diff --git a/docs/guide-ru/test-overview.md b/docs/guide-ru/test-overview.md index 3c7668e..544993a 100644 --- a/docs/guide-ru/test-overview.md +++ b/docs/guide-ru/test-overview.md @@ -14,7 +14,7 @@ Процесс разработки фичи следующий: -- Создать новый тест, который описывает функцию, которая будет реализована. +- Создать новый тест, описывающий функцию, которая будет реализована. - Запустить новый тест и убедиться, что он терпит неудачу. Это ожидаемо, т.к. на данный момент еще нет конкретной реализации. - Написать простой код, чтобы новый тест отрабатывал без ошибок. - Запустить все тесты и убедиться, что они отрабатывают без ошибок diff --git a/docs/guide-ru/test-unit.md b/docs/guide-ru/test-unit.md index 9859e13..868f502 100644 --- a/docs/guide-ru/test-unit.md +++ b/docs/guide-ru/test-unit.md @@ -15,7 +15,8 @@ Запуск тестов шаблонов проектов basic и advanced ------------------------------------------------ -Следуйте инструкциям в `apps/advanced/tests/README.md` и `apps/basic/tests/README.md`. +- [Инструкции для шаблона advanced](https://github.com/yiisoft/yii2-app-advanced/blob/master/docs/guide/start-testing.md). +- [Инструкции для шаблона basic](https://github.com/yiisoft/yii2-app-basic/blob/master/README.md#testing). Модульные тесты фреймворка -------------------------- diff --git a/docs/guide-ru/tutorial-i18n.md b/docs/guide-ru/tutorial-i18n.md index fad413a..d010588 100644 --- a/docs/guide-ru/tutorial-i18n.md +++ b/docs/guide-ru/tutorial-i18n.md @@ -497,7 +497,7 @@ class TranslationEventHandler Переводы могут храниться в [[yii\i18n\PhpMessageSource|PHP-файлах]], [[yii\i18n\GettextMessageSource|файлах .po]] или в [[yii\i18n\DbMessageSource|базе данных]]. См. соответствующие классы для дополнительных опций. -Прежде всего, вам необходимо создать конфигурационный файл. Решите где вы хотите хранить его и затем выполните команду +Прежде всего, вам необходимо создать конфигурационный файл. Решите, где вы хотите хранить его и затем выполните команду ```bash ./yii message/config-template path/to/config.php @@ -557,7 +557,7 @@ class TranslationEventHandler Для работы с большей частью функций интернационализации Yii использует [PHP-расширение intl](https://secure.php.net/manual/ru/book.intl.php). Например, это расширение используют классы, отвечающие за форматирование чисел и дат [[yii\i18n\Formatter]] и за форматирование строк [[yii\i18n\MessageFormatter]]. Оба класса поддерживают базовый функционал даже в том случае, если расширение `intl` не -установлено. Однако, этот запасной вариант более-менее будет работать только для сайтов на английском языке, хотя даже для +установлено. Однако этот запасной вариант более или менее будет работать только для сайтов на английском языке, хотя даже для них большая часть широких возможностей расширения `intl` не будет доступна, поэтому его установка настоятельно рекомендуется. [PHP-расширение intl](https://secure.php.net/manual/ru/book.intl.php) основано на [библиотеке ICU](http://site.icu-project.org/), которая diff --git a/docs/guide-ru/tutorial-performance-tuning.md b/docs/guide-ru/tutorial-performance-tuning.md index 090f225..fa9162b 100644 --- a/docs/guide-ru/tutorial-performance-tuning.md +++ b/docs/guide-ru/tutorial-performance-tuning.md @@ -19,7 +19,7 @@ При запуске приложения в производственном режиме, вам нужно отключить режим отладки. Yii использует значение константы `YII_DEBUG` чтобы указать, следует ли включить режим отладки. Когда режим отладки включен, Yii тратит дополнительное -время чтобы создать и записать отладочную информацию. +время, чтобы создать и записать отладочную информацию. Вы можете разместить следующую строку кода в начале [входного скрипта](structure-entry-scripts.md) чтобы отключить режим отладки: @@ -154,7 +154,7 @@ CREATE TABLE session ( ## Использование обычных массивов Хотя [Active Record](db-active-record.md) очень удобно использовать, это не так эффективно, как использование простых -массивов, когда вам нужно получить большое количество данных из БД. В этом случае, вы можете вызвать `asArray()` при +массивов, когда вам нужно получить большое количество данных из БД. В этом случае вы можете вызвать `asArray()` при использовании Active Record для получения данных, чтобы извлеченные данные были представлены в виде массивов вместо громоздких записей Active Record. Например, @@ -189,7 +189,7 @@ composer dumpautoload -o ## Асинхронная обработка данных -Когда запрос включает в себя некоторые ресурсоемкие операции, вы должны подумать о том, чтобы выполнить эти операции +Когда запрос включает в себя некоторые ресурсоёмкие операции, вы должны подумать о том, чтобы выполнить эти операции асинхронно, не заставляя пользователя ожидать их окончания. Существует два метода асинхронной обработки данных: pull и push. diff --git a/docs/guide-ru/tutorial-yii-as-micro-framework.md b/docs/guide-ru/tutorial-yii-as-micro-framework.md index a4e5f2f..39a7b25 100644 --- a/docs/guide-ru/tutorial-yii-as-micro-framework.md +++ b/docs/guide-ru/tutorial-yii-as-micro-framework.md @@ -122,7 +122,7 @@ micro-app/ ## Создание REST API -Чтобы продемонстрировать использование нашей "микроархитектуры" мы создадим простой REST API для сообщений. +Чтобы продемонстрировать использование нашей "микроархитектуры", мы создадим простой REST API для сообщений. Чтобы этот API обслуживал некоторые данные, нам нужна база данных. Добавим конфигурацию подключения базы данных к конфигурации приложения: diff --git a/docs/guide-vi/intro-yii.md b/docs/guide-vi/intro-yii.md index 96538e0..bd2fc08 100644 --- a/docs/guide-vi/intro-yii.md +++ b/docs/guide-vi/intro-yii.md @@ -2,7 +2,7 @@ Yii là gì =========== Yii là một PHP Framework mã nguồn mở và hoàn toàn miễn phí, có hiệu năng xử lý cao, phát triển tốt nhất trên các ứng dụng Web 2.0, sử dụng tối đa các thành phần (component-based PHP framework) để tăng tốc độ viết ứng dụng. -Tên Yii (được phát âm là `Yee` hoặc `[ji:]`) ở Trung Quốc có nghĩa là "thật đơn giản và luôn phát triển". Nghĩa thứ hai có thể đọc ngắn gọn là **Yes It Is**! +Tên Yii (được phát âm là `Yee` hoặc `[ji:]`) ở Trung Quốc có nghĩa là "thật đơn giản và luôn phát triển" (Hán tự "易", âm "dịch"). Nghĩa thứ hai có thể đọc ngắn gọn là **Yes It Is**! Yii thích hợp nhất để làm gì? diff --git a/docs/guide-zh-CN/README.md b/docs/guide-zh-CN/README.md index cf450d8..ddb2baa 100644 --- a/docs/guide-zh-CN/README.md +++ b/docs/guide-zh-CN/README.md @@ -79,10 +79,10 @@ Yii 2.0 权威指南 * [查询生成器(Query Builder)](db-query-builder.md): 使用简单抽象层查询数据库 * [活动记录(Active Record)](db-active-record.md): 活动记录对象关系映射(ORM),检索和操作记录、定义关联关系 * [数据库迁移(Migrations)](db-migrations.md): 在团体开发中对你的数据库使用版本控制 -* [Sphinx](https://github.com/yiisoft/yii2-sphinx/blob/master/docs/guide-zh-CN/README.md) -* [Redis(yii2-redis)](yii2-redis.md): yii2-redis 扩展详解 -* [MongoDB](https://github.com/yiisoft/yii2-mongodb/blob/master/docs/guide-zh-CN/README.md) -* [ElasticSearch](https://github.com/yiisoft/yii2-elasticsearch/blob/master/docs/guide-zh-CN/README.md) +* [Sphinx](https://www.yiiframework.com/extension/yiisoft/yii2-sphinx/doc/guide) +* [Redis(yii2-redis)](https://www.yiiframework.com/extension/yiisoft/yii2-redis/doc/guide/2.0/zh-cn): yii2-redis 扩展详解 +* [MongoDB](https://www.yiiframework.com/extension/yiisoft/yii2-mongodb/doc/guide) +* [ElasticSearch](https://www.yiiframework.com/extension/yiisoft/yii2-elasticsearch/doc/guide) 接收用户数据(Getting Data from Users) diff --git a/docs/guide-zh-CN/concept-di-container.md b/docs/guide-zh-CN/concept-di-container.md index 511cb80..81ebff6 100644 --- a/docs/guide-zh-CN/concept-di-container.md +++ b/docs/guide-zh-CN/concept-di-container.md @@ -427,7 +427,7 @@ $container->setDefinitions([ } ]); -$reader = $container->get('app\storage\DocumentsReader); +$reader = $container->get('app\storage\DocumentsReader'); // 将按照配置中的描述创建 DocumentReader 对象及其依赖关系 ``` @@ -465,7 +465,7 @@ $container->setDefinitions([ ] ]); -$reader = $container->get('app\storage\DocumentsReader); +$reader = $container->get('app\storage\DocumentsReader'); // 将与前面示例中的行为完全相同。 ``` diff --git a/docs/guide-zh-CN/concept-properties.md b/docs/guide-zh-CN/concept-properties.md index ec9fc5f..9dff1da 100644 --- a/docs/guide-zh-CN/concept-properties.md +++ b/docs/guide-zh-CN/concept-properties.md @@ -16,11 +16,11 @@ $object->label = trim($label); 就不得不修改所有给 `label` 属性赋值的代码。这种代码的重复会导致 bug, 这种实践显然需要尽可能避免。 -为解决该问题,Yii 引入了一个名为 [[yii\base\Object]] 的基类, +为解决该问题,Yii 引入了一个名为 [[yii\base\BaseObject]] 的基类, 它支持基于类内的 *getter* 和 *setter*(读取器和设定器)方法来定义属性。 -如果某类需要支持这个特性,只需要继承 [[yii\base\Object]] 或其子类即可。 +如果某类需要支持这个特性,只需要继承 [[yii\base\BaseObject]] 或其子类即可。 -> Info: 几乎每个 Yii 框架的核心类都继承自 [[yii\base\Object]] 或其子类。 +> Info: 几乎每个 Yii 框架的核心类都继承自 [[yii\base\BaseObject]] 或其子类。 这意味着只要在核心类中见到 getter 或 setter 方法,就可以像调用属性一样调用它。 getter 方法是名称以 `get` 开头的方法,而 setter 方法名以 `set` 开头。 diff --git a/docs/guide-zh-CN/rest-response-formatting.md b/docs/guide-zh-CN/rest-response-formatting.md index b053a59..e5a9876 100644 --- a/docs/guide-zh-CN/rest-response-formatting.md +++ b/docs/guide-zh-CN/rest-response-formatting.md @@ -164,7 +164,7 @@ JSON 响应将由 [[yii\web\JsonResponseFormatter|JsonResponseFormatter]] 类来 比如 [[yii\web\JsonResponseFormatter::$prettyPrint|$prettyPrint]],这对于开发更好的可读式响应更有用, 或者用 [[yii\web\JsonResponseFormatter::$encodeOptions|$encodeOptions]] 去控制 JSON 编码的输出。 -格式化程序可以在 [configuration](concept-configuration.md) 的 `response` 应用程序组件 [[yii\web\Response::formatters|formatters]] 的属性进行配置, +格式化程序可以在 [configuration](concept-configurations.md) 的 `response` 应用程序组件 [[yii\web\Response::formatters|formatters]] 的属性进行配置, 如下所示: ```php diff --git a/docs/guide-zh-CN/test-environment-setup.md b/docs/guide-zh-CN/test-environment-setup.md index 8af0c19..64213bc 100644 --- a/docs/guide-zh-CN/test-environment-setup.md +++ b/docs/guide-zh-CN/test-environment-setup.md @@ -8,8 +8,8 @@ Yii 2 官方兼容 [`Codeception`](https://github.com/Codeception/Codeception) - [功能测试](test-functional.md) - 在浏览器模拟器中以用户视角来验证期望的场景是否发生 - [验收测试](test-acceptance.md) - 在真实的浏览器中以用户视角验证期望的场景是否发生。 -Yii 为包括 [`yii2-basic`](https://github.com/yiisoft/yii2/tree/master/apps/basic) 和 -[`yii2-advanced`](https://github.com/yiisoft/yii2/tree/master/apps/advanced) +Yii 为包括 [`yii2-basic`](https://github.com/yiisoft/yii2-app-basic) 和 +[`yii2-advanced`](https://github.com/yiisoft/yii2-app-advanced) 在内的应用模板脚手架提供全部三种类型的即用测试套件。 Codeception 预装了基本和高级项目模板。 diff --git a/docs/guide/concept-di-container.md b/docs/guide/concept-di-container.md index 3e0c601..0493bae 100644 --- a/docs/guide/concept-di-container.md +++ b/docs/guide/concept-di-container.md @@ -169,6 +169,9 @@ $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer'); // to create an instance of Connection $container->set('foo', 'yii\db\Connection'); +// register an alias with `Instance::of` +$container->set('bar', Instance::of('foo')); + // register a class with configuration. The configuration // will be applied when the class is instantiated by get() $container->set('yii\db\Connection', [ @@ -179,7 +182,7 @@ $container->set('yii\db\Connection', [ ]); // register an alias name with class configuration -// In this case, a "class" element is required to specify the class +// In this case, a "class" or "__class" element is required to specify the class $container->set('db', [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', @@ -188,11 +191,12 @@ $container->set('db', [ 'charset' => 'utf8', ]); -// register a PHP callable +// register callable closure or array // The callable will be executed each time when $container->get('db') is called $container->set('db', function ($container, $params, $config) { return new \yii\db\Connection($config); }); +$container->set('db', ['app\db\DbFactory', 'create']); // register a component instance // $container->get('pageCache') will return the same instance each time it is called @@ -215,7 +219,6 @@ $container->setSingleton('yii\db\Connection', [ ]); ``` - Resolving Dependencies ---------------------- @@ -238,6 +241,9 @@ $db = $container->get('db'); // equivalent to: $engine = new \app\components\SearchEngine($apiKey, $apiSecret, ['type' => 1]); $engine = $container->get('app\components\SearchEngine', [$apiKey, $apiSecret], ['type' => 1]); + +// equivalent to: $api = new \app\components\Api($host, $apiKey); +$api = $container->get('app\components\Api', ['host' => $host, 'apiKey' => $apiKey]); ``` Behind the scene, the DI container does much more work than just creating a new object. @@ -372,6 +378,24 @@ cannot be instantiated. This is because you need to tell the DI container how to Now if you access the controller again, an instance of `app\components\BookingService` will be created and injected as the 3rd parameter to the controller's constructor. +Since Yii 2.0.36 when using PHP 7 action injection is available for both web and console controllers: + +```php +namespace app\controllers; + +use yii\web\Controller; +use app\components\BookingInterface; + +class HotelController extends Controller +{ + public function actionBook($id, BookingInterface $bookingService) + { + $result = $bookingService->book($id); + // ... + } +} +``` + Advanced Practical Usage --------------- @@ -440,32 +464,27 @@ we shall copy-paste the line that creates `FileStorage` object, that is not the As described in the [Resolving Dependencies](#resolving-dependencies) subsection, [[yii\di\Container::set()|set()]] and [[yii\di\Container::setSingleton()|setSingleton()]] can optionally take dependency's constructor parameters as -a third argument. To set the constructor parameters, you may use the following configuration array format: - - - `key`: class name, interface name or alias name. The key will be passed to the - [[yii\di\Container::set()|set()]] method as a first argument `$class`. - - `value`: array of two elements. The first element will be passed to the [[yii\di\Container::set()|set()]] method as the - second argument `$definition`, the second one — as `$params`. +a third argument. To set the constructor parameters, you may use the `__construct()` option: Let's modify our example: ```php $container->setDefinitions([ 'tempFileStorage' => [ // we've created an alias for convenience - ['class' => 'app\storage\FileStorage'], - ['/var/tempfiles'] // could be extracted from some config files + 'class' => 'app\storage\FileStorage', + '__construct()' => ['/var/tempfiles'], // could be extracted from some config files ], 'app\storage\DocumentsReader' => [ - ['class' => 'app\storage\DocumentsReader'], - [Instance::of('tempFileStorage')] + 'class' => 'app\storage\DocumentsReader', + '__construct()' => [Instance::of('tempFileStorage')], ], 'app\storage\DocumentsWriter' => [ - ['class' => 'app\storage\DocumentsWriter'], - [Instance::of('tempFileStorage')] + 'class' => 'app\storage\DocumentsWriter', + '__construct()' => [Instance::of('tempFileStorage')] ] ]); -$reader = $container->get('app\storage\DocumentsReader); +$reader = $container->get('app\storage\DocumentsReader'); // Will behave exactly the same as in the previous example. ``` @@ -488,19 +507,19 @@ create its instance once and use it multiple times. ```php $container->setSingletons([ 'tempFileStorage' => [ - ['class' => 'app\storage\FileStorage'], - ['/var/tempfiles'] + 'class' => 'app\storage\FileStorage', + '__construct()' => ['/var/tempfiles'] ], ]); $container->setDefinitions([ 'app\storage\DocumentsReader' => [ - ['class' => 'app\storage\DocumentsReader'], - [Instance::of('tempFileStorage')] + 'class' => 'app\storage\DocumentsReader', + '__construct()' => [Instance::of('tempFileStorage')], ], 'app\storage\DocumentsWriter' => [ - ['class' => 'app\storage\DocumentsWriter'], - [Instance::of('tempFileStorage')] + 'class' => 'app\storage\DocumentsWriter', + '__construct()' => [Instance::of('tempFileStorage')], ] ]); diff --git a/docs/guide/db-active-record.md b/docs/guide/db-active-record.md index 23a8376..ca3e19f 100644 --- a/docs/guide/db-active-record.md +++ b/docs/guide/db-active-record.md @@ -1567,7 +1567,7 @@ values from received data set. You are able to fetch additional columns or values from query and store it inside the Active Record. For example, assume we have a table named `room`, which contains information about rooms available in the hotel. Each room stores information about its geometrical size using fields `length`, `width`, `height`. -Imagine we need to retrieve list of all available rooms with their volume in descendant order. +Imagine we need to retrieve list of all available rooms with their volume in descending order. So you can not calculate volume using PHP, because we need to sort the records by its value, but you also want `volume` to be displayed in the list. To achieve the goal, you need to declare an extra field in your `Room` Active Record class, which will store `volume` value: diff --git a/docs/guide/db-dao.md b/docs/guide/db-dao.md index a050407..98351e8 100644 --- a/docs/guide/db-dao.md +++ b/docs/guide/db-dao.md @@ -19,12 +19,6 @@ In Yii 2.0, DAO supports the following databases out of the box: - [Oracle](http://www.oracle.com/us/products/database/overview/index.html) - [MSSQL](https://www.microsoft.com/en-us/sqlserver/default.aspx): version 2008 or higher. -> Info: In Yii 2.1 and later, the DAO supports for CUBRID, Oracle and MSSQL are no longer provided as the built-in - core components of the framework. They have to be installed as the separated [extensions](structure-extensions.md). - There are [yiisoft/yii2-oracle](https://www.yiiframework.com/extension/yiisoft/yii2-oracle) and - [yiisoft/yii2-mssql](https://www.yiiframework.com/extension/yiisoft/yii2-mssql) in the - [official extensions](https://www.yiiframework.com/extensions/official). - > Note: New version of pdo_oci for PHP 7 currently exists only as the source code. Follow [instruction provided by community](https://github.com/yiisoft/yii2/issues/10975#issuecomment-248479268) to compile it or use [PDO emulation layer](https://github.com/taq/pdooci). @@ -113,6 +107,18 @@ and [[yii\db\Connection::password|password]]. Please refer to [[yii\db\Connectio > ], > ``` +For MS SQL Server additional connection option is needed for proper binary data handling: + +```php +'db' => [ + 'class' => 'yii\db\Connection', + 'dsn' => 'sqlsrv:Server=localhost;Database=mydatabase', + 'attributes' => [ + \PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_SYSTEM + ] +], +``` + ## Executing SQL Queries diff --git a/docs/guide/db-query-builder.md b/docs/guide/db-query-builder.md index 9c23f69..7a9db30 100644 --- a/docs/guide/db-query-builder.md +++ b/docs/guide/db-query-builder.md @@ -231,7 +231,7 @@ Using the Hash Format, Yii internally applies parameter binding for values, so i here you do not have to add parameters manually. However, note that Yii never escapes column names, so if you pass a variable obtained from user side as a column name without any additional checks, the application will become vulnerable to SQL injection attack. In order to keep the application secure, either do not use variables as column names or -filter variable against white list. In case you need to get column name from user, read the [Filtering Data](output-data-widgets.md#filtering-data) +filter variable against allowlist. In case you need to get column name from user, read the [Filtering Data](output-data-widgets.md#filtering-data) guide article. For example the following code is vulnerable: ```php @@ -321,7 +321,7 @@ the operator can be one of the following: Using the Operator Format, Yii internally uses parameter binding for values, so in contrast to the [string format](#string-format), here you do not have to add parameters manually. However, note that Yii never escapes column names, so if you pass a variable as a column name, the application will likely become vulnerable to SQL injection attack. In order to keep -application secure, either do not use variables as column names or filter variable against white list. +application secure, either do not use variables as column names or filter variable against allowlist. In case you need to get column name from user, read the [Filtering Data](output-data-widgets.md#filtering-data) guide article. For example the following code is vulnerable: @@ -602,6 +602,28 @@ $query1->union($query2); You can call [[yii\db\Query::union()|union()]] multiple times to append more `UNION` fragments. +### [[yii\db\Query::withQuery()|withQuery()]] + +The [[yii\db\Query::withQuery()|withQuery()]] method specifies the `WITH` prefix of a SQL query. You can use it instead of subquery for more readability and some unique features (recursive CTE). Read more at [modern-sql](https://modern-sql.com/feature/with). For example, this query will select all nested permissions of `admin` with their children recursively, + +```php +$initialQuery = (new \yii\db\Query()) + ->select(['parent', 'child']) + ->from(['aic' => 'auth_item_child']) + ->where(['parent' => 'admin']); + +$recursiveQuery = (new \yii\db\Query()) + ->select(['aic.parent', 'aic.child']) + ->from(['aic' => 'auth_item_child']) + ->innerJoin('t1', 't1.child = aic.parent'); + +$mainQuery = (new \yii\db\Query()) + ->select(['parent', 'child']) + ->from('t1') + ->withQuery($initialQuery->union($recursiveQuery), 't1', true); +``` + +[[yii\db\Query::withQuery()|withQuery()]] can be called multiple times to prepend more CTE's to main query. Queries will be prepend in same order as they attached. If one of query is recursive then whole CTE become recursive. ## Query Methods @@ -694,6 +716,8 @@ $query = (new \yii\db\Query()) ->all(); ``` +The column name passed into [[yii\db\Query::indexBy()|indexBy()]] method has to be a part of the `SELECT` fragment of a SQL statement. If [[yii\db\Query::select()|select()]] is not used, all columns are selected and therefore the condition is met. If [[yii\db\Query::select()|select()]] is used with an array in its parameter, Yii handles adding that required SQL fragment for you. This applies when using [[yii\db\Query::indexBy()|indexBy()]] with [[yii\db\Query::all()|all()]] or [[yii\db\Query::column()|column()]]. In other cases, like the following example with an anonymous function, is up to users themselves to take care of it. + To index by expression values, pass an anonymous function to the [[yii\db\Query::indexBy()|indexBy()]] method: ```php diff --git a/docs/guide/helper-html.md b/docs/guide/helper-html.md index 3e6601a..1c411e7 100644 --- a/docs/guide/helper-html.md +++ b/docs/guide/helper-html.md @@ -327,18 +327,18 @@ echo Html::getAttributeName('dates[0]'); There are two methods to generate tags wrapping embedded styles and scripts: ```php - + 'print']) ?> Gives you - + - true]) ?> + Gives you - + ``` If you want to use an external style in a CSS file: diff --git a/docs/guide/input-validation.md b/docs/guide/input-validation.md index c5f6268..30f5137 100644 --- a/docs/guide/input-validation.md +++ b/docs/guide/input-validation.md @@ -355,8 +355,10 @@ the method/function is: * @param mixed $params the value of the "params" given in the rule * @param \yii\validators\InlineValidator $validator related InlineValidator instance. * This parameter is available since version 2.0.11. + * @param mixed $current the currently validated value of attribute. + * This parameter is available since version 2.0.36. */ -function ($attribute, $params, $validator) +function ($attribute, $params, $validator, $current) ``` If an attribute fails the validation, the method/function should call [[yii\base\Model::addError()]] to save diff --git a/docs/guide/output-data-widgets.md b/docs/guide/output-data-widgets.md index e009fea..ea9c253 100644 --- a/docs/guide/output-data-widgets.md +++ b/docs/guide/output-data-widgets.md @@ -335,6 +335,7 @@ To add a CheckboxColumn to the GridView, add it to the [[yii\grid\GridView::$col ```php echo GridView::widget([ + 'id' => 'grid', 'dataProvider' => $dataProvider, 'columns' => [ // ... diff --git a/docs/guide/rest-quick-start.md b/docs/guide/rest-quick-start.md index c68e668..3e5fe4c 100644 --- a/docs/guide/rest-quick-start.md +++ b/docs/guide/rest-quick-start.md @@ -162,7 +162,9 @@ Content-Type: application/xml The following command will create a new user by sending a POST request with the user data in JSON format: ``` -$ curl -i -H "Accept:application/json" -H "Content-Type:application/json" -XPOST "http://localhost/users" -d '{"username": "example", "email": "user@example.com"}' +$ curl -i -H "Accept:application/json" -H "Content-Type:application/json" \ + -XPOST "http://localhost/users" \ + -d '{"username": "example", "email": "user@example.com"}' HTTP/1.1 201 Created ... diff --git a/docs/guide/rest-resources.md b/docs/guide/rest-resources.md index d5fd9a5..164cbf5 100644 --- a/docs/guide/rest-resources.md +++ b/docs/guide/rest-resources.md @@ -82,7 +82,7 @@ public function fields() } // filter out some fields, best used when you want to inherit the parent implementation -// and blacklist some sensitive fields. +// and exclude some sensitive fields. public function fields() { $fields = parent::fields(); diff --git a/docs/guide/rest-response-formatting.md b/docs/guide/rest-response-formatting.md index 5daa707..bfc5955 100644 --- a/docs/guide/rest-response-formatting.md +++ b/docs/guide/rest-response-formatting.md @@ -165,7 +165,7 @@ better readable responses, or [[yii\web\JsonResponseFormatter::$encodeOptions|$e of the JSON encoding. The formatter can be configured in the [[yii\web\Response::formatters|formatters]] property of the `response` application -component in the application [configuration](concept-configuration.md) like the following: +component in the application [configuration](concept-configurations.md) like the following: ```php 'response' => [ diff --git a/docs/guide/runtime-logging.md b/docs/guide/runtime-logging.md index 815af1d..041150a 100644 --- a/docs/guide/runtime-logging.md +++ b/docs/guide/runtime-logging.md @@ -142,8 +142,8 @@ the pattern `yii\db\*`. If you do not specify the [[yii\log\Target::categories|categories]] property, it means the target will process messages of *any* category. -Besides whitelisting the categories by the [[yii\log\Target::categories|categories]] property, you may also -blacklist certain categories by the [[yii\log\Target::except|except]] property. If the category of a message +In addition to specifying allowed categories using the [[yii\log\Target::categories|categories]] property, you may also +exclude certain categories by the [[yii\log\Target::except|except]] property. If the category of a message is found or matches one of the patterns in this property, it will NOT be processed by the target. The following target configuration specifies that the target should only process error and warning messages diff --git a/docs/guide/runtime-requests.md b/docs/guide/runtime-requests.md index 8a648b2..bce3c3c 100644 --- a/docs/guide/runtime-requests.md +++ b/docs/guide/runtime-requests.md @@ -200,3 +200,27 @@ With the above configuration, all headers listed in `secureHeaders` are filtered except the `X-ProxyUser-Ip` and `Front-End-Https` headers in case the request is made by the proxy. In that case the former is used to retrieve the user IP as configured in `ipHeaders` and the latter will be used to determine the result of [[yii\web\Request::getIsSecureConnection()]]. + +Since 2.0.31 [RFC 7239](https://tools.ietf.org/html/rfc7239) `Forwarded` header is supported. In order to enable +it you need to add header name to `secureHeaders`. Make sure your proxy is setting it, otherwise end user would be +able to spoof IP and protocol. + +### Already resolved user IP + +If the user's IP address is resolved before the Yii application (e.g. `ngx_http_realip_module` or similar), +the `request` component will work correctly with the following configuration: + +```php +'request' => [ + // ... + 'trustedHosts' => [ + '0.0.0.0/0', + ], + 'ipHeaders' => [], +], +``` + +In this case, the value of [[yii\web\Request::userIP|userIP]] will be equal to `$_SERVER['REMOTE_ADDR']`. +Also, properties that are resolved from HTTP headers will work correctly (e.g. [[yii\web\Request::getIsSecureConnection()]]). + +> Warning: The `trustedHosts=['0.0.0.0/0']` setting assumes that all IPs are trusted. diff --git a/docs/guide/runtime-sessions-cookies.md b/docs/guide/runtime-sessions-cookies.md index facde1c..384b62b 100644 --- a/docs/guide/runtime-sessions-cookies.md +++ b/docs/guide/runtime-sessions-cookies.md @@ -397,3 +397,10 @@ To use this feature across different PHP versions check the version first. E.g. ``` > Note: Since not all browsers support the `sameSite` setting yet, it is still strongly recommended to also include [additional CSRF protection](security-best-practices.md#avoiding-csrf). + +## Session php.ini settings + +As [noted in PHP manual](https://www.php.net/manual/en/session.security.ini.php), `php.ini` has important +session security settings. Please ensure recommended settings are applied. Especially `session.use_strict_mode` +that is not enabled by default in PHP installations. +This setting can also be set with [[yii\web\Session::useStrictMode]]. diff --git a/docs/guide/security-best-practices.md b/docs/guide/security-best-practices.md index 03549d4..1695655 100644 --- a/docs/guide/security-best-practices.md +++ b/docs/guide/security-best-practices.md @@ -353,3 +353,27 @@ return [ > Note: you should always prefer web server configuration for 'host header attack' protection instead of the filter usage. [[yii\filters\HostControl]] should be used only if server configuration setup is unavailable. + +### Configuring SSL peer validation + +There is a typical misconception about how to solve SSL certificate validation issues such as: + +``` +cURL error 60: SSL certificate problem: unable to get local issuer certificate +``` + +or + +``` +stream_socket_enable_crypto(): SSL operation failed with code 1. OpenSSL Error messages: error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed +``` + +Many sources wrongly suggest disabling SSL peer verification. That should not be ever done since it enabled +man-in-the middle type of attacks. Instead, PHP should be configured properly: + +1. Download [https://curl.haxx.se/ca/cacert.pem](https://curl.haxx.se/ca/cacert.pem). +2. Add the following to your php.ini: + ``` + openssl.cafile="/path/to/cacert.pem" + curl.cainfo="/path/to/cacert.pem". + ``` diff --git a/docs/guide/start-databases.md b/docs/guide/start-databases.md index e2e0cad..111f79e 100644 --- a/docs/guide/start-databases.md +++ b/docs/guide/start-databases.md @@ -178,16 +178,19 @@ class CountryController extends Controller Save the above code in the file `controllers/CountryController.php`. -The `index` action calls `Country::find()`. This Active Record method builds a DB query that can be used to retrieve all of the data from the `country` table. -To limit the number of countries returned in each request, the query is paginated with the help of a +First, The `index` action calls `Country::find()`. This [find()](https://www.yiiframework.com/doc/api/2.0/yii-db-activerecord#find()-detail) method creates a [ActiveQuery](https://www.yiiframework.com/doc/api/2.0/yii-db-activequery) query object, which provides methods to access data from the `country` table. + +To limit the number of countries returned in each request, the query object is paginated with the help of a [[yii\data\Pagination]] object. The `Pagination` object serves two purposes: -* Sets the `offset` and `limit` clauses for the SQL statement represented by the query so that it only +* Sets the `offset` and `limit` clauses for the SQL statement represented by the query object so that it only returns a single page of data at a time (at most 5 rows in a page). * It's used in the view to display a pager consisting of a list of page buttons, as will be explained in the next subsection. + +Next, [all()](https://www.yiiframework.com/doc/api/2.0/yii-db-activequery#all()-detail) returns all `country` records based on the query results. -At the end of the code, the `index` action renders a view named `index`, and passes the country data as well as the pagination +At the end of the code, the `index` action renders a view named `index`, and passes the returned country data as well as the pagination information to it. diff --git a/docs/guide/start-installation.md b/docs/guide/start-installation.md index 473a2bd..7d14124 100644 --- a/docs/guide/start-installation.md +++ b/docs/guide/start-installation.md @@ -140,7 +140,7 @@ Verifying the Installation After installation is done, either configure your web server (see next section) or use the [built-in PHP web server](https://secure.php.net/manual/en/features.commandline.webserver.php) by running the following -console command while in the project `web` directory: +console command while in the project root directory: ```bash php yii serve @@ -291,9 +291,69 @@ in order to avoid many unnecessary system `stat()` calls. Also note that when running an HTTPS server, you need to add `fastcgi_param HTTPS on;` so that Yii can properly detect if a connection is secure. +### Recommended NGINX Unit Configuration + +You can run Yii-based apps using [NGINX Unit](https://unit.nginx.org/) with a PHP language module. +Here is a sample configuration. + +```json +{ + "listeners": { + "*:80": { + "pass": "routes/yii" + } + }, + + "routes": { + "yii": [ + { + "match": { + "uri": [ + "!/assets/*", + "*.php", + "*.php/*" + ] + }, + + "action": { + "pass": "applications/yii/direct" + } + }, + { + "action": { + "share": "/path/to/app/web/", + "fallback": { + "pass": "applications/yii/index" + } + } + } + ] + }, + + "applications": { + "yii": { + "type": "php", + "user": "www-data", + "targets": { + "direct": { + "root": "/path/to/app/web/" + }, + + "index": { + "root": "/path/to/app/web/", + "script": "index.php" + } + } + } + } +} +``` + +You can also [set up](https://unit.nginx.org/configuration/#php) your PHP environment or supply a custom `php.ini` in the same configuration. + ### IIS Configuration -It's recommended to host the application in a virtual host where document root points to `path/to/app/web` folder. In that `web` folder you have to place a file named `web.config` i.e. `path/to/app/web/web.config`. Content of the file should be the following: +It's recommended to host the application in a virtual host (Web site) where document root points to `path/to/app/web` folder and that Web site is configured to run PHP. In that `web` folder you have to place a file named `web.config` i.e. `path/to/app/web/web.config`. Content of the file should be the following: ```xml @@ -317,3 +377,6 @@ It's recommended to host the application in a virtual host where document root p ``` +Also the following list of Microsoft's official resources could be useful in order to configure PHP on IIS: + 1. [How to set up your first IIS Web site](https://support.microsoft.com/en-us/help/323972/how-to-set-up-your-first-iis-web-site) + 2. [Configure a PHP Website on IIS](https://docs.microsoft.com/en-us/iis/application-frameworks/scenario-build-a-php-website-on-iis/configure-a-php-website-on-iis) diff --git a/docs/guide/start-looking-ahead.md b/docs/guide/start-looking-ahead.md index b496480..a9ba1b3 100644 --- a/docs/guide/start-looking-ahead.md +++ b/docs/guide/start-looking-ahead.md @@ -27,7 +27,7 @@ This section will summarize the Yii resources available to help you be more prod * Community - Forum: - IRC chat: The #yii channel on the freenode network () - - Slack chanel: + - Slack chanel: - Gitter chat: - GitHub: - Facebook: diff --git a/docs/guide/structure-filters.md b/docs/guide/structure-filters.md index 8590c74..0789814 100644 --- a/docs/guide/structure-filters.md +++ b/docs/guide/structure-filters.md @@ -34,7 +34,7 @@ public function behaviors() By default, filters declared in a controller class will be applied to *all* actions in that controller. You can, however, explicitly specify which actions the filter should be applied to by configuring the [[yii\base\ActionFilter::only|only]] property. In the above example, the `HttpCache` filter only applies to the -`index` and `view` actions. You can also configure the [[yii\base\ActionFilter::except|except]] property to blacklist +`index` and `view` actions. You can also configure the [[yii\base\ActionFilter::except|except]] property to prevent some actions from being filtered. Besides controllers, you can also declare filters in a [module](structure-modules.md) or [application](structure-applications.md). diff --git a/docs/guide/structure-models.md b/docs/guide/structure-models.md index dc2d24b..1cfc9bb 100644 --- a/docs/guide/structure-models.md +++ b/docs/guide/structure-models.md @@ -478,7 +478,7 @@ public function fields() } // filter out some fields, best used when you want to inherit the parent implementation -// and blacklist some sensitive fields. +// and exclude some sensitive fields. public function fields() { $fields = parent::fields(); diff --git a/docs/guide/test-environment-setup.md b/docs/guide/test-environment-setup.md index 3c738d3..e69ef3c 100644 --- a/docs/guide/test-environment-setup.md +++ b/docs/guide/test-environment-setup.md @@ -17,7 +17,7 @@ In case you are not using one of these templates, Codeception could be installed by issuing the following console commands: ``` -composer require codeception/codeception -composer require codeception/specify -composer require codeception/verify +composer require --dev codeception/codeception +composer require --dev codeception/specify +composer require --dev codeception/verify ``` diff --git a/docs/guide/tutorial-core-validators.md b/docs/guide/tutorial-core-validators.md index d731ba9..cce4191 100644 --- a/docs/guide/tutorial-core-validators.md +++ b/docs/guide/tutorial-core-validators.md @@ -82,7 +82,7 @@ is as specified by the `operator` property. is being used to validate an attribute, the default value of this property would be the name of the attribute suffixed with `_repeat`. For example, if the attribute being validated is `password`, then this property will default to `password_repeat`. -- `compareValue`: a constant value that the input value should be compared with. When both +- `compareValue`: a constant value (or a closure returning a value) that the input value should be compared with. When both of this property and `compareAttribute` are specified, this property will take precedence. - `operator`: the comparison operator. Defaults to `==`, meaning checking if the input value is equal to that of `compareAttribute` or `compareValue`. The following operators are supported: @@ -155,7 +155,8 @@ specified via [[yii\validators\DateValidator::timestampAttribute|timestampAttrib [[yii\validators\DateValidator::$timestampAttributeTimeZone|$timestampAttributeTimeZone]]. Note, that when using `timestampAttribute`, the input value will be converted to a unix timestamp, which by definition is in UTC, so - a conversion from the [[yii\validators\DateValidator::timeZone|input time zone]] to UTC will be performed. + a conversion from the [[yii\validators\DateValidator::timeZone|input time zone]] to UTC will be performed (this behavior + can be changed by setting [[yii\validators\DateValidator::$defaultTimeZone|$defaultTimeZone]] since 2.0.39). - Since version 2.0.4 it is also possible to specify a [[yii\validators\DateValidator::$min|minimum]] or [[yii\validators\DateValidator::$max|maximum]] timestamp. @@ -191,8 +192,8 @@ or `1970-01-01` in the input field of a date picker. This validator does not validate data. Instead, it assigns a default value to the attributes being validated if the attributes are empty. -- `value`: the default value or a PHP callable that returns the default value which will be assigned to - the attributes being validated if they are empty. The signature of the PHP callable should be as follows, +- `value`: the default value or a closure as callback that returns the default value which will be assigned to + the attributes being validated if they are empty. The signature of the closure should be as follows, ```php function foo($model, $attribute) { @@ -270,24 +271,49 @@ This validator checks if the input value is a valid email address. ```php [ // a1 needs to exist in the column represented by the "a1" attribute + // i.e. a1 = 1, valid if there is value 1 in column "a1" ['a1', 'exist'], + // equivalent of + ['a1', 'exist', 'targetAttribute' => 'a1'], + ['a1', 'exist', 'targetAttribute' => ['a1' => 'a1']], // a1 needs to exist, but its value will use a2 to check for the existence + // i.e. a1 = 2, valid if there is value 2 in column "a2" ['a1', 'exist', 'targetAttribute' => 'a2'], + // equivalent of + ['a1', 'exist', 'targetAttribute' => ['a1' => 'a2']], + + // a2 needs to exist, its value will use a2 to check for the existence, a1 will receive error message + // i.e. a2 = 2, valid if there is value 2 in column "a2" + ['a1', 'exist', 'targetAttribute' => ['a2']], + // equivalent of + ['a1', 'exist', 'targetAttribute' => ['a2' => 'a2']], // a1 and a2 need to exist together, and they both will receive error message + // i.e. a1 = 3, a2 = 4, valid if there is value 3 in column "a1" and value 4 in column "a2" [['a1', 'a2'], 'exist', 'targetAttribute' => ['a1', 'a2']], + // equivalent of + [['a1', 'a2'], 'exist', 'targetAttribute' => ['a1' => 'a1', 'a2' => 'a2']], // a1 and a2 need to exist together, only a1 will receive error message + // i.e. a1 = 5, a2 = 6, valid if there is value 5 in column "a1" and value 6 in column "a2" ['a1', 'exist', 'targetAttribute' => ['a1', 'a2']], + // equivalent of + ['a1', 'exist', 'targetAttribute' => ['a1' => 'a1', 'a2' => 'a2']], // a1 needs to exist by checking the existence of both a2 and a3 (using a1 value) + // i.e. a1 = 7, a2 = 8, valid if there is value 7 in column "a3" and value 8 in column "a2" ['a1', 'exist', 'targetAttribute' => ['a2', 'a1' => 'a3']], + // equivalent of + ['a1', 'exist', 'targetAttribute' => ['a2' => 'a2', 'a1' => 'a3']], // a1 needs to exist. If a1 is an array, then every element of it must exist. + // i.e. a1 = 9, valid if there is value 9 in column "a1" + // a1 = [9, 10], valid if there are values 9 and 10 in column "a1" ['a1', 'exist', 'allowArray' => true], - // type_id needs to exist in the column "id" in the table defined in ProductType class + // type_id needs to exist in the column "id" in the table defined in ProductType class + // i.e. type_id = 1, valid if there is value 1 in column "id" of ProductType's table ['type_id', 'exist', 'targetClass' => ProductType::class, 'targetAttribute' => ['type_id' => 'id']], // the same as the previous, but using already defined relation "type" @@ -309,10 +335,19 @@ multiple attribute values should exist). of the input value. If not set, it will use the name of the attribute currently being validated. You may use an array to validate the existence of multiple columns at the same time. The array values are the attributes that will be used to validate the existence, while the array keys are the attributes - whose values are to be validated. If the key and the value are the same, you can just specify the value. + whose values are to be validated. If the key, and the value are the same, you can just specify the value. + Assuming we have ModelA to be validated and ModelB set as the target class the following `targetAttribute`'s + configurations are taken as: + - `null` => value of a currently validated attribute of ModelA will be checked against stored values of ModelB's attribute with the same name + - `'a'` => value of a currently validated attribute of ModelA will be checked against stored values of attribute "a" of ModelB + - `['a']` => value of attribute "a" of ModelA will be checked against stored values of attribute "a" of ModelB + - `['a' => 'a']` => the same as above + - `['a', 'b']` => value of attribute "a" of ModelA will be checked against stored values of attribute "a" of ModelB and + at the same time value of attribute "b" of ModelA will be checked against stored values of attribute "b" of ModelB + - `['a' => 'b']` => value of attribute "a" of ModelA will be checked against stored values of attribute "b" of ModelB - `targetRelation`: since version 2.0.14 you can use convenient attribute `targetRelation`, which overrides the `targetClass` and `targetAttribute` attributes using specs from the requested relation. -- `filter`: additional filter to be applied to the DB query used to check the existence of the input value. - This can be a string or an array representing the additional query condition (refer to [[yii\db\Query::where()]] +- `filter`: an additional filter to be applied to the DB query used to check the existence of the input value. + This can be a string, or an array representing the additional query condition (refer to [[yii\db\Query::where()]] on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query` is the [[yii\db\Query|Query]] object that you can modify in the function. - `allowArray`: whether to allow the input value to be an array. Defaults to `false`. If this property is `true` @@ -638,19 +673,34 @@ the input value. Note that if the input value is an array, it will be ignored by ```php [ // a1 needs to be unique in the column represented by the "a1" attribute + // i.e. a1 = 1, valid if there is no value 1 in column "a1" ['a1', 'unique'], + // equivalent of + ['a1', 'unique', 'targetAttribute' => 'a1'], + ['a1', 'unique', 'targetAttribute' => ['a1' => 'a1']], // a1 needs to be unique, but column a2 will be used to check the uniqueness of the a1 value + // i.e. a1 = 2, valid if there is no value 2 in column "a2" ['a1', 'unique', 'targetAttribute' => 'a2'], + // equivalent of + ['a1', 'unique', 'targetAttribute' => ['a1' => 'a2']], // a1 and a2 need to be unique together, and they both will receive error message + // i.e. a1 = 3, a2 = 4, valid if there is no value 3 in column "a1" and at the same time no value 4 in column "a2" [['a1', 'a2'], 'unique', 'targetAttribute' => ['a1', 'a2']], + // equivalent of + [['a1', 'a2'], 'unique', 'targetAttribute' => ['a1' => 'a1', 'a2' => 'a2']], // a1 and a2 need to be unique together, only a1 will receive error message ['a1', 'unique', 'targetAttribute' => ['a1', 'a2']], // a1 needs to be unique by checking the uniqueness of both a2 and a3 (using a1 value) + // i.e. a1 = 5, a2 = 6, valid if there is no value 5 in column "a3" and at the same time no value 6 in column "a2" ['a1', 'unique', 'targetAttribute' => ['a2', 'a1' => 'a3']], + + // type_id needs to be unique in the column "id" in the table defined in ProductType class + // i.e. type_id = 1, valid if there is no value 1 in column "id" of ProductType's table + ['type_id', 'unique', 'targetClass' => ProductType::class, 'targetAttribute' => 'id'], ] ``` @@ -664,9 +714,18 @@ either a single column or multiple columns. of the input value. If not set, it will use the name of the attribute currently being validated. You may use an array to validate the uniqueness of multiple columns at the same time. The array values are the attributes that will be used to validate the uniqueness, while the array keys are the attributes - whose values are to be validated. If the key and the value are the same, you can just specify the value. -- `filter`: additional filter to be applied to the DB query used to check the uniqueness of the input value. - This can be a string or an array representing the additional query condition (refer to [[yii\db\Query::where()]] + whose values are to be validated. If the key, and the value are the same, you can just specify the value. + Assuming we have ModelA to be validated and ModelB set as the target class the following `targetAttribute`'s + configurations are taken as: + - `null` => value of a currently validated attribute of ModelA will be checked against stored values of ModelB's attribute with the same name + - `'a'` => value of a currently validated attribute of ModelA will be checked against stored values of attribute "a" of ModelB + - `['a']` => value of attribute "a" of ModelA will be checked against stored values of attribute "a" of ModelB + - `['a' => 'a']` => the same as above + - `['a', 'b']` => value of attribute "a" of ModelA will be checked against stored values of attribute "a" of ModelB and + at the same time value of attribute "b" of ModelA will be checked against stored values of attribute "b" of ModelB + - `['a' => 'b']` => value of attribute "a" of ModelA will be checked against stored values of attribute "b" of ModelB +- `filter`: an additional filter to be applied to the DB query used to check the uniqueness of the input value. + This can be a string, or an array representing the additional query condition (refer to [[yii\db\Query::where()]] on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query` is the [[yii\db\Query|Query]] object that you can modify in the function. diff --git a/docs/guide/tutorial-docker.md b/docs/guide/tutorial-docker.md index 24eea57..61cad4e 100644 --- a/docs/guide/tutorial-docker.md +++ b/docs/guide/tutorial-docker.md @@ -3,7 +3,7 @@ Yii and Docker For development and deployments Yii applications can be run as Docker containers. A container is like a lightweight isolated virtual machine that maps its services to host's ports, i.e. a webserver in a container on port 80 is available on port 8888 on your (local)host. -Containers can solve many issues such as having identical software versions at developer's computer and the server, fast deployments or simulating mutli-server architecture while developing. +Containers can solve many issues such as having identical software versions at developer's computer and the server, fast deployments or simulating multi-server architecture while developing. You can read more about Docker containers on [docker.com](https://www.docker.com/what-docker). diff --git a/docs/guide/tutorial-mailing.md b/docs/guide/tutorial-mailing.md index 7daba27..98ee1e8 100644 --- a/docs/guide/tutorial-mailing.md +++ b/docs/guide/tutorial-mailing.md @@ -23,6 +23,15 @@ return [ 'components' => [ 'mailer' => [ 'class' => 'yii\swiftmailer\Mailer', + 'useFileTransport' => false, + 'transport' => [ + 'class' => 'Swift_SmtpTransport', + 'encryption' => 'tls', + 'host' => 'your_mail_server_host', + 'port' => 'your_smtp_port', + 'username' => 'your_username', + 'password' => 'your_password', + ], ], ], ]; diff --git a/docs/internals-ja/git-workflow.md b/docs/internals-ja/git-workflow.md index 6f63fdf..8eed245 100644 --- a/docs/internals-ja/git-workflow.md +++ b/docs/internals-ja/git-workflow.md @@ -217,7 +217,7 @@ git push origin --delete 999-name-of-your-branch-goes-here ### 注意: -退行 (regression) を早期に発見するために、github 上の Yii コード・ベースへのマージは、すべて [Travis CI](http://travis-ci.org) に取り上げられて、自動化されたテストにかけられます。 +退行 (regression) を早期に発見するために、github 上の Yii コード・ベースへのマージは、すべて [Travis CI](http://travis-ci.com) に取り上げられて、自動化されたテストにかけられます。 コア・チームとしては、このサービスに過大な負担をかけたくないために、以下の場合にはマージの説明に [`[ci skip]`](https://docs.travis-ci.com/user/customizing-the-build/#Skipping-a-build) が含まれるようにしてください。 すなわち、プル・リクエストが下記のものである場合がそうです。 diff --git a/docs/internals-pl/git-workflow.md b/docs/internals-pl/git-workflow.md index 6ec9b88..7dc2c57 100644 --- a/docs/internals-pl/git-workflow.md +++ b/docs/internals-pl/git-workflow.md @@ -224,7 +224,7 @@ git push origin --delete 999-nazwa-twojej-galezi-w-tym-miejscu ### Note: W celu wczesnego wykrycia ewentualnych problemów z integracją, każde żądanie scalenia głównego kodu Yii na GitHubie jest -weryfikowane przez automatyczne testy [Travis CI](http://travis-ci.org). Ponieważ ekipa głównych programistów stara się nie +weryfikowane przez automatyczne testy [Travis CI](http://travis-ci.com). Ponieważ ekipa głównych programistów stara się nie nadużywać tej usługi, [`[ci skip]`](https://docs.travis-ci.com/user/customizing-the-build/#Skipping-a-build) jest dodawane przy komentarzu scalenia kodu, jeśli żądanie: diff --git a/docs/internals-ru/git-workflow.md b/docs/internals-ru/git-workflow.md index 1e4878b..1c0e741 100644 --- a/docs/internals-ru/git-workflow.md +++ b/docs/internals-ru/git-workflow.md @@ -221,7 +221,7 @@ git push origin --delete 999-name-of-your-branch-goes-here ### Примечание Для обнаружения регрессии как можно раньше, каждое слияние кодовой базы Yii на Github будет подхвачено -[Travis CI](http://travis-ci.org) для автоматического запуска тестов. Люди из *core team* не хотят нагружать +[Travis CI](http://travis-ci.com) для автоматического запуска тестов. Люди из *core team* не хотят нагружать этот сервис, поэтому добавляют текст [`[ci skip]`](https://docs.travis-ci.com/user/customizing-the-build/#Skipping-a-build) в описание запроса на слияние, в следующих ситуациях: diff --git a/docs/internals-sr-Latn/git-workflow.md b/docs/internals-sr-Latn/git-workflow.md index 6d67df1..08ae802 100644 --- a/docs/internals-sr-Latn/git-workflow.md +++ b/docs/internals-sr-Latn/git-workflow.md @@ -173,7 +173,7 @@ git push origin --delete 999-IME-VASE-GRANE ### Napomena: -Kako bi rano otkrili regresije u Yii kodu prilikom svake integracije na GitHub-u pokreće se [Travis CI](http://travis-ci.org) kako bi se radilo testiranje. Pošto Yii tim ne želi da preoptereti ovaj servis, +Kako bi rano otkrili regresije u Yii kodu prilikom svake integracije na GitHub-u pokreće se [Travis CI](http://travis-ci.com) kako bi se radilo testiranje. Pošto Yii tim ne želi da preoptereti ovaj servis, [`[ci skip]`](https://docs.travis-ci.com/user/customizing-the-build/#Skipping-a-build) će biti uključen prilikom svake integracije ako pull zahtev: * utiče samo na javascript, css i slike, diff --git a/docs/internals-uk/git-workflow.md b/docs/internals-uk/git-workflow.md index 3dfb3ed..140d9a5 100644 --- a/docs/internals-uk/git-workflow.md +++ b/docs/internals-uk/git-workflow.md @@ -202,7 +202,7 @@ git push origin --delete 999-name-of-your-branch-goes-here ### Примітка: Для виявлення регресу на ранніх стадіях кожне поєднання з кодовою базою Yii на GitHub опрацьовується у -[Travis CI](http://travis-ci.org) для автоматичного запуску тестів. Оскільки основна команда розробників не бажає +[Travis CI](http://travis-ci.com) для автоматичного запуску тестів. Оскільки основна команда розробників не бажає перевантажувати сервіс, додавайте [`[ci skip]`](https://docs.travis-ci.com/user/customizing-the-build/#Skipping-a-build) до опису поєднання, якщо ваш "pull request": diff --git a/docs/internals/git-workflow.md b/docs/internals/git-workflow.md index 1016e69..f61a306 100644 --- a/docs/internals/git-workflow.md +++ b/docs/internals/git-workflow.md @@ -218,7 +218,7 @@ git push origin --delete 999-name-of-your-branch-goes-here ### Note: To detect regressions early every merge to the Yii codebase on GitHub will be picked up by -[Travis CI](http://travis-ci.org) for an automated testrun. As core team doesn't wish to overtax this service, +[Travis CI](http://travis-ci.com) for an automated testrun. As core team doesn't wish to overtax this service, [`[ci skip]`](https://docs.travis-ci.com/user/customizing-the-build/#Skipping-a-build) will be included to the merge description if the pull request: diff --git a/framework/.github/CONTRIBUTING.md b/framework/.github/CONTRIBUTING.md new file mode 100644 index 0000000..c6c50c5 --- /dev/null +++ b/framework/.github/CONTRIBUTING.md @@ -0,0 +1,4 @@ +Contributing to Yii 2 +===================== + +Please use [yiisoft/yii2](https://github.com/yiisoft/yii2) to report issues and send pull requests. Thank you! diff --git a/framework/.github/FUNDING.yml b/framework/.github/FUNDING.yml new file mode 100644 index 0000000..d6f1e3b --- /dev/null +++ b/framework/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# These are supported funding model platforms + +open_collective: yiisoft +github: [yiisoft] +tidelift: "packagist/yiisoft/yii2" diff --git a/framework/.github/PULL_REQUEST_TEMPLATE.md b/framework/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e7a5435 --- /dev/null +++ b/framework/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ + diff --git a/framework/.github/SECURITY.md b/framework/.github/SECURITY.md new file mode 100644 index 0000000..f713847 --- /dev/null +++ b/framework/.github/SECURITY.md @@ -0,0 +1,6 @@ +# Security Policy + +Please use the [security issue form](https://www.yiiframework.com/security) to report to us any security issue you find in Yii. +DO NOT use the issue tracker or discuss it in the public forum as it will cause more damage than help. + +Please note that as a non-commerial OpenSource project we are not able to pay bounties at the moment. diff --git a/framework/.phpstorm.meta.php b/framework/.phpstorm.meta.php new file mode 100644 index 0000000..c6663ce --- /dev/null +++ b/framework/.phpstorm.meta.php @@ -0,0 +1,26 @@ + '@', + ]) + ); + + override( + \yii\di\Instance::ensure(0), + map([ + '' => '@', + ]) + ); + + override( + \Yii::createObject(0), + map([ + '' => '@', + ]) + ); +} diff --git a/framework/BaseYii.php b/framework/BaseYii.php index fa27c7b..f24f5ed 100644 --- a/framework/BaseYii.php +++ b/framework/BaseYii.php @@ -93,7 +93,7 @@ class BaseYii */ public static function getVersion() { - return '2.0.27-dev'; + return '2.0.41-dev'; } /** @@ -130,7 +130,7 @@ class BaseYii */ public static function getAlias($alias, $throwException = true) { - if (strncmp($alias, '@', 1)) { + if (strpos($alias, '@') !== 0) { // not an alias return $alias; } @@ -278,7 +278,7 @@ class BaseYii { if (isset(static::$classMap[$className])) { $classFile = static::$classMap[$className]; - if ($classFile[0] === '@') { + if (strpos($classFile, '@') === 0) { $classFile = static::getAlias($classFile); } } elseif (strpos($className, '\\') !== false) { @@ -343,17 +343,29 @@ class BaseYii { if (is_string($type)) { return static::$container->get($type, $params); - } elseif (is_array($type) && isset($type['class'])) { + } + + if (is_callable($type, true)) { + return static::$container->invoke($type, $params); + } + + if (!is_array($type)) { + throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type)); + } + + if (isset($type['__class'])) { + $class = $type['__class']; + unset($type['__class'], $type['class']); + return static::$container->get($class, $params, $type); + } + + if (isset($type['class'])) { $class = $type['class']; unset($type['class']); return static::$container->get($class, $params, $type); - } elseif (is_callable($type, true)) { - return static::$container->invoke($type, $params); - } elseif (is_array($type)) { - throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); } - throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type)); + throw new InvalidConfigException('Object configuration must be an array containing a "class" or "__class" element.'); } private static $_logger; diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 4569008..f723027 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -1,15 +1,309 @@ Yii Framework 2 Change Log ========================== -2.0.27 under development +2.0.41 under development ------------------------ +- Enh #18499: When using `yii\db\Query::all()` and `yii\db\Query::$indexBy`, the `yii\db\Query::$indexBy` is auto inserted into `yii\db\Query::$select` - the same as in `yii\db\Query::column()` (OndrejVasicek, samdark, bizley) +- Enh #18483: Add `yii\log\Logger::$dbEventNames` that allows specifying event names used to get statistical results (profiling) of DB queries (atiline) +- Enh #18455: Add ability to use separate attributes for data model and filter model of `yii\grid\GridView` in `yii\grid\DataColumn` (PowerGamer1) +- Enh #18447: Do not use `getLastInsertID` to get PK from insert query to lower collision probability for concurrent inserts (darkdef) +- Bug #18448: Fix issues in queries and tests for older MSSQL versions (darkdef) +- Enh #18460: `compareValue` in `CompareValidator` can now take a closure returning a value (mmonem) +- Bug #18464: Fix bug with processing fallback messages when translation language is set to `null` (bizley) +- Enh #18457: Add `EVENT_RESET` and `EVENT_FINISH` events to `yii\db\BatchQueryResult` (brandonkelly) +- Bug #18472: Fix initializing `db` component configuration in `yii\data\ActiveDataProvider` (bizley) +- Bug #18479: Fix invalid argument type for `preg_split` in `\yii\console\Controller` (gazooz) +- Bug #18477: Fix detecting availability of Xdebug's stack trace in `yii\base\ErrorException` (bizley) +- Bug #18480: Transactions are not committed using the dblib driver (bbrunekreeft) +- Enh #18493: Faster request parsing for REST UrlRule with prefix handling (bizley) +- Enh #18487: Allow creating URLs for non-GET-verb rules (bizley) +- Bug #8750: Fix MySQL support when running in `ANSI`/`ANSI_QUOTES` modes (brandonkelly) +- Bug #18505: Fixed `yii\helpers\ArrayHelper::getValue()` for ArrayAccess objects with explicitly defined properties (samdark) +- Bug #18508: Fix Postgres SQL query for load table indexes with correct column order (insolita) +- Enh #18518: Add support for ngrok’s `X-Original-Host` header (brandonkelly) +- Bug #18529: Fix asset files path with `appendTimestamp` option for non-root-relative base URLs (bizley) +- Bug #18450: Allow empty string to be passed as a nullable typed argument to a controller's action (dicrtarasov, bizley) +- Bug #18535: Set Cookie SameSite to Lax by default (samdark) +- Bug #18539: Fix "driver does not support quoting" when using the driver pdo_odbc (xpohoc69) + + +2.0.40 December 23, 2020 +------------------------ + +- Bug #16492: Fix eager loading Active Record relations when relation key is a subject to a type-casting behavior (bizley) +- Bug #18199: Fix content body response on 304 HTTP status code, according to RFC 7232 (rad8329) +- Bug #18287: Fix the OUTPUT got SQL syntax error if the column name is MSSQL keyword e.g. key (darkdef) +- Bug #18339: Fix migrate controller actions to return exit codes (haohetao, bizley) +- Bug #18365: Move quoting of table names to upper level to function `getSchemaMetadata()` in MSSQL driver to get clean names from the schema (darkdef) +- Bug #18383: RBAC's generated file made PSR-12 compliant (perlexed) +- Bug #18386: Fix `assets/yii.activeForm.js` incorrect target selector for `validatingCssClass` (brussens) +- Bug #18393: Fix `ActiveRecord::refresh()` to load data from the database even if cache is enabled (hooman-mirghasemi) +- Bug #18395: Fix regression in `yii\helpers\BaseArrayHelper::filter()` (allowing filtering arrays with numeric keys) (bizley) +- Bug #18400: Set parent module of the newly attached child module by `Module::setModule()` and `Module::setModules()` (sup-ham) +- Bug #18406: Fix PDO exception when committing or rolling back an autocommitted transaction in PHP 8 (brandonkelly) +- Bug #18414: Fix `AssetManager::appendTimestamp()` not appending timestamp for website root in sub-directory (Isitar) +- Bug #18426: Fix check for route's leading slash in `yii\widgets\Menu` (stevekr) +- Bug #18435: Fix ensuring Active Record relation links' keys to be strings (bizley) +- Bug #18437: Change the check order whether an object is an implementation of `Arrayable` or `JsonSerializable` in `\yii\base\ArrayableTrait::toArray()` and `\yii\rest\Serializer::serialize()` (spell6inder) +- Bug #18442: Fix calls with array access to string (bizley) +- Enh #18381: The `yii\web\AssetManager` `$basePath` readable and writeable check has been moved to the `checkBasePathPermission()`. This check will run once before `publishFile()` and `publishDirectory()` (nadar) +- Enh #18394: Add support for setting `yii\web\Response::$stream` to a callable (brandonkelly) + + +2.0.39.3 November 23, 2020 +-------------------------- + +- Bug #18396: Fix not throw `InvalidConfigException` when failed to instantiate class via DI container in some cases (vjik) +- Enh #18200: Add `maxlength` attribute by default to the input text when it is an active field within a `yii\grid\DataColumn` (rad8329) + + +2.0.39.2 November 13, 2020 +-------------------------- + +- Bug #18378: Fix not taking default value when unable to resolve abstract class via DI container (vjik) + + +2.0.39.1 November 10, 2020 +-------------------------- + +- Bug #18373: Fix not taking default value when unable to resolve non-existing class via DI container (vjik) +- Enh #18370: Add option to provide a string replacement for `null` value in `yii\data\DataFilter` (bizley) + + +2.0.39 November 10, 2020 +------------------------ + +- Bug #16418: Fix `yii\data\Pagination::getLinks()` to return links to the first and the last pages regardless of the current page (ptz-nerf, bizley) +- Bug #16831: Fix console Table widget does not render correctly in combination with ANSI formatting (issidorov, cebe) +- Bug #18160, #18192: Fix `registerFile` with set argument `depends` does not take `position` and `appendTimestamp` into account (baleeny) +- Bug #18263: Fix writing `\yii\caching\FileCache` files to the same directory when `keyPrefix` is set (githubjeka) +- Bug #18287: Fix for `OUTPUT INSERTED` and computed columns. Add flag to mark computed values in table schema (darkdef) +- Bug #18290: Fix response with non-seekable streams (schmunk42) +- Bug #18297: Replace usage of deprecated `ReflectionParameter::isArray()` method in PHP8 (baletskyi) +- Bug #18303: Fix creating migration for column methods used after `defaultValues` (wsaid) +- Bug #18308: Fix `\yii\base\Model::getErrorSummary()` reverse order (DrDeath72) +- Bug #18313: Fix multipart form data parsing with double quotes (wsaid) +- Bug #18317: Additional PHP 8 compatibility fixes (samdark, bizley) +- Enh #18247: Add support for the 'session.use_strict_mode' ini directive in `yii\web\Session` (rhertogh) +- Enh #18285: Enhanced DI container to allow passing parameters by name in a constructor (vjik) +- Enh #18304: Add support of constructor parameters with default values to DI container (vjik) +- Enh #18351: Add option to change default timezone for parsing formats without time part in `yii\validators\DateValidator` (bizley) + + +2.0.38 September 14, 2020 +------------------------- + +- Bug #13973: Correct alterColumn for MSSQL & drop constraints before dropping a column (darkdef) +- Bug #15265: PostgreSQL > 10.0 is not pass tests with default value of timestamp CURRENT_TIMESTAMP (terabytesoftw) +- Bug #16892: Validation error class was not applied to checkbox and radio when validationStateOn = self::VALIDATION_STATE_ON_INPUT (dan-szabo, samdark) +- Bug #18040: Display width specification for integer data types was deprecated in MySQL 8.0.19 (terabytesoftw) +- Bug #18066: Fix `yii\db\Query::create()` wasn't using all info from `withQuery()` (maximkou) +- Bug #18229: Add a flag to specify SyBase database when used with pdo_dblib (darkdef) +- Bug #18232: Fail tests pgsql v-10.14, v-11.9, v-12-latest (terabytesoftw) +- Bug #18233: Add PHP 8 support (samdark) +- Bug #18239: Fix support of no-extension files for `FileValidator::validateExtension()` (darkdef) +- Bug #18245: Make resolving DI references inside of arrays in dependencies optional (SamMousa, samdark, hiqsol) +- Bug #18248: Render only one stack trace on a console for chained exceptions (mikehaertl) +- Bug #18269: Fix integer safe attribute to work properly in `yii\base\Model` (Ladone) +- Bug: (CVE-2020-15148): Disable unserialization of `yii\db\BatchQueryResult` to prevent remote code execution in case application calls unserialize() on user input containing specially crafted string (samdark, russtone) +- Enh #18196: `yii\rbac\DbManager::$checkAccessAssignments` is now `protected` (alex-code) +- Enh #18213: Do not load fixtures with circular dependencies twice instead of throwing an exception (JesseHines0) +- Enh #18236: Allow `yii\filters\RateLimiter` to accept a closure function for the `$user` property in order to assign values on runtime (nadar) + + +2.0.37 August 07, 2020 +---------------------- + +- Bug #17147: Fix form attribute validations for empty select inputs (kartik-v) +- Bug #18156: Fix `yii\db\Schema::quoteSimpleTableName()` was checking incorrect quote character (M4tho, samdark) +- Bug #18170: Fix 2.0.36 regression in passing extra console command arguments to the action (darkdef) +- Bug #18171: Change case of column names in SQL query for `findConstraints` to fix MySQL 8 compatibility (darkdef) +- Bug #18182: `yii\db\Expression` was not supported as condition in `ActiveRecord::findOne()` and `ActiveRecord::findAll()` (rhertogh) +- Bug #18189: Fix "Invalid parameter number" in `yii\rbac\DbManager::removeItem()` (samdark) +- Bug #18198: Fix saving tables with trigger by outputting inserted data from insert query with usage of temporary table (darkdef) +- Bug #18203: PDO exception code was not properly passed to `yii\db\Exception` (samdark) +- Bug #18204: Fix 2.0.36 regression in inline validator and JS validation (samdark) +- Enh #18205: Add `.phpstorm.meta.php` file for better auto-completion in PhpStorm (vjik) +- Enh #18210: Allow strict comparison for multi-select inputs (alex-code) +- Enh #18217: Make `yii\console\ErrorHandler` render chained exceptions in debug mode (mikehaertl) + + +2.0.36 July 07, 2020 +-------------------- + +- Bug #13828: Fix retrieving inserted data for a primary key of type uniqueidentifier for SQL Server 2005 or later (darkdef) +- Bug #17474: Fix retrieving inserted data for a primary key of type trigger for SQL Server 2005 or later (darkdef) +- Bug #17985: Convert migrationNamespaces to array if needed (darkdef) +- Bug #18001: Fix getting table metadata for tables `(` in their name (floor12) +- Bug #18026: Fix `ArrayHelper::getValue()` did not work with `ArrayAccess` objects (mikk150) +- Bug #18028: Fix division by zero exception in console `Table::calculateRowHeight()` (fourhundredfour) +- Bug #18031: `HttpBasicAuth` with auth callback now triggers login events same was as other authentication methods (samdark) +- Bug #18041: Fix RBAC migration for MSSQL (darkdef) +- Bug #18047: Fix colorization markers output in console `Table` (cheeseq) +- Bug #18051: Fix missing support for custom validation method in EachValidator (bizley) +- Bug #18051: Fix using `EachValidator` with custom validation function (bizley) +- Bug #18081: Fix for PDO_DBLIB/MSSQL. Set flag `ANSI_NULL_DFLT_ON` to ON for current DB connection (darkdef) +- Bug #18086: Fix accessing public properties of `ArrayAccess` via `ArrayHelper::getValue()` (samdark) +- Bug #18094: Add support for composite file extension validation (darkdef) +- Bug #18096: Fix `InlineValidator` with anonymous inline function not working well from `EachValidator` (trombipeti) +- Bug #18101: Fix behavior of `OUTPUT INSERTED.*` for SQL Server query: "insert default values"; correct MSSQL unit tests; turn off profiling echo message in migration test (darkdef) +- Bug #18105: Fix for old trigger in RBAC migration with/without `prefixTable` (darkdef) +- Bug #18110: Add quotes to return value of viewName in MSSQL schema. It is `[someView]` now (darkdef) +- Bug #18127: Resolve DI references inside of arrays in dependencies (hiqsol) +- Bug #18134: `Expression` as `columnName` should not be quoted in `likeCondition` (darkdef) +- Bug #18147: Fix parameters binding for MySQL when prepare emulation is off (rskrzypczak) +- Enh #15202: Add optional param `--silent-exit-on-exception` in `yii\console\Controller` (egorrishe) +- Enh #17722: Add action injection support (SamMousa, samdark, erickskrauch) +- Enh #18019: Allow jQuery 3.5.0 to be installed (wouter90) +- Enh #18048: Use `Instance::ensure()` to set `User::$accessChecker` (lav45) +- Enh #18083: Add `Controller::$request` and `$response` (brandonkelly) +- Enh #18120: Include the path to the log file into error message if `FileTarget::export` fails (uaoleg) +- Enh #18151: Add `Mutex::isAcquired()` to check if lock is currently acquired (rhertogh) + + +2.0.35 May 02, 2020 +------------------- + +- Bug #16481: Fix RBAC MSSQL trigger (achretien) +- Bug #17653: Fix `TypeError: pair[1] is undefined` when query param doesn't have `=` sign (baso10) +- Bug #17810: Fix `EachValidator` crashing with uninitialized typed properties (ricardomm85) +- Bug #17942: Fix for `DbCache` loop in MySQL `QueryBuilder` (alex-code) +- Bug #17948: Ignore errors caused by `set_time_limit(0)` (brandonkelly) +- Bug #17960: Fix unsigned primary key type mapping for SQLite (bizley) +- Bug #17961: Fix pagination `pageSizeLimit` ignored if set as array with more then 2 elements (tsvetiligo) +- Bug #17974: Fix `ActiveRelationTrait` compatibility with PHP 7.4 (Ximich) +- Bug #17975: Fix deleting unused messages with console command if message tables were created manually (auerswald, cebe) +- Bug #17991: Improve `yii\db\Connection` master and slave failover, no connection attempt was made when all servers are marked as unavailable (cebe) +- Bug #18000: PK value of Oracle ActiveRecord is missing after save (mankwok) +- Bug #18010: Allow upper or lower case operators in `InCondition` and `LikeCondition` (alex-code) +- Bug #18011: Add attribute labels support for `DynamicModel`, fixed `EachValidator` to pass the attribute label to the underlying `DynamicModel` (storch) +- Enh #17758: `Query::withQuery()` can now be used for CTE (sartor) +- Enh #17993: Add `yii\i18n\Formatter::$currencyDecimalSeparator` to allow setting custom symbols for currency decimal in `IntlNumberFormatter` (XPOHOC269) +- Enh #18006: Allow `SameSite` cookie pre PHP 7.3 (scottix) + + +2.0.34 March 26, 2020 +--------------------- + +- Bug #17932: Fix regression in detection of AJAX requests (samdark) +- Bug #17933: Log warning instead of erroring when URLManager is unable to initialize cache (samdark) +- Bug #17934: Fix regression in Oracle when binding several string parameters (fen1xpv, samdark) +- Bug #17935: Reset DB quoted table/column name caches when the connection is closed (brandonkelly) + + +2.0.33 March 24, 2020 +--------------------- + +- Bug #11945: Fix Schema Builder MySQL column definition order (simialbi) +- Bug #13749: Fix Yii opens db connection even when hits query cache (shushenghong) +- Bug #16092: Fix duplicate joins in usage of `joinWith` (germanow) +- Bug #16145: Fix `Html` helper `checkboxList()`, `radioList()`, `renderSelectOptions()`, `dropDownList()`, `listBox()` methods to work properly with traversable selection (samdark) +- Bug #16334: Add `\JsonSerializable` support to `ArrayableTrait` (germanow) +- Bug #17667: Fix `CREATE INDEX` failure on SQLite when specifying schema (santilin, samdark) +- Bug #17679: Fix Oracle exception "ORA-01461: can bind a LONG value only for insert into a LONG column" when inserting 4k+ string (vinpel, 243083df) +- Bug #17797: Fix for `activeListInput` options (alex-code) +- Bug #17798: Avoid creating directory for stream log targets in `FileTarget` (wapmorgan) +- Bug #17828: Fix `yii\web\UploadedFile::saveAs()` failing when error value in `$_FILES` entry is a string (haveyaseen) +- Bug #17829: `yii\helpers\ArrayHelper::filter` now correctly filters data when passing a filter with more than 2 levels (rhertogh) +- Bug #17843: Fix `yii\web\Session::setCookieParamsInternal` checked "samesite" parameter incorrectly (schevgeny) +- Bug #17850: Update to `ReplaceArrayValue` config exception message (alex-code) +- Bug #17859: Fix loading fixtures under Windows (samdark) +- Bug #17863: `\yii\helpers\BaseInflector::slug()` doesn't work with an empty string as a replacement argument (haruatari) +- Bug #17875: Use `move_uploaded_file()` function instead of `copy()` and `unlink()` for saving uploaded files in case of POST request (sup-ham) +- Bug #17878: Detect CORS AJAX requests without `X-Requested-With` in `Request::getIsAjax()` (dicrtarasov, samdark) +- Bug #17881: `yii\db\Query::queryScalar()` wasn’t reverting the `select`, `orderBy`, `limit`, and `offset` params if an exception occurred (brandonkelly) +- Bug #17884: Fix 0 values in console Table rendered as empty string (mikehaertl) +- Bug #17886: Fix `yii\rest\Serializer` to serialize arrays (patacca) +- Bug #17909: Reset DB schema, transaction, and driver name when the connection is closed (brandonkelly) +- Bug #17920: Fix quoting for `Command::getRawSql` having `Expression` in params (alex-code) +- Enh #7622: Allow `yii\data\ArrayDataProvider` to control the sort flags for `sortModels` through `yii\data\Sort::sortFlags` property (askobara) +- Enh #16721: Use `Instance::ensure()` to initialize `UrlManager::$cache` (rob006) +- Enh #17827: Add `StringValidator::$strict` that can be turned off to allow any scalars (adhayward, samdark) +- Enh #17929: Actions can now have bool typed params bound (alex-code) + + +2.0.32 January 21, 2020 +----------------------- + +- Bug #12539: `yii\filters\ContentNegotiator` now generates 406 'Not Acceptable' instead of 415 'Unsupported Media Type' on content-type negotiation fail (PowerGamer1) +- Bug #17037: Fix uploaded file saving method when data came from `MultipartFormDataParser` (sup-ham) +- Bug #17300: Fix class-level event handling with wildcards (Toma91) +- Bug #17635: Fix varbinary data handling for MSSQL (toatall) +- Bug #17744: Fix a bug with setting incorrect `defaultValue` to AR column with `CURRENT_TIMESTAMP(x)` as default expression (MySQL >= 5.6.4) (bizley) +- Bug #17749: Fix logger dispatcher behavior when target crashes in PHP 7.0+ (kamarton) +- Bug #17755: Fix a bug for web request with `trustedHosts` set to format `['10.0.0.1' => ['X-Forwarded-For']]` (shushenghong) +- Bug #17760: Fix `JSON::encode()` for `\DateTimeInterface` under PHP 7.4 (samdark) +- Bug #17762: PHP 7.4: Remove special condition for converting PHP errors to exceptions if they occurred inside of `__toString()` call (rob006) +- Bug #17766: Remove previous PJAX event binding before registering new one (samdark) +- Bug #17767: Make `Formatter::formatNumber` method protected (TheCodeholic) +- Bug #17771: migrate/fresh was not returning exit code (samdark) +- Bug #17793: Fix inconsistent handling of null `data` attribute values in `yii\helpers\BaseHtml::renderTagAttributes()` (brandonkelly) +- Bug #17803: Fix `ErrorHandler` unregister and register to only change global state when applicable (SamMousa) +- Enh #17729: Path alias support was added to `yii\web\UploadedFile::saveAs()` (sup-ham) +- Enh #17792: Add support for `aria` attributes to `yii\helpers\BaseHtml::renderTagAttributes()` (brandonkelly) + +2.0.31 December 18, 2019 +------------------------ + +- Bug #17661: Fix query builder incorrect IN/NOT IN condition handling for null values (strychu) +- Bug #17685: Fix invalid db component in `m180523_151638_rbac_updates_indexes_without_prefix` (rvkulikov) +- Bug #17687: `Query::indexBy` can now include a table alias (brandonkelly) +- Bug #17694: Fixed Error Handler to clear registered view tags, scripts, and files when rendering error view through action view (bizley) +- Bug #17701: Throw `BadRequestHttpException` when request params can’t be bound to `int` and `float` controller action arguments (brandonkelly) +- Bug #17710: Fix MemCache duration normalization to avoid memcached/system timestamp mismatch (samdark) +- Bug #17723: Fix `Model::activeAttributes()` to access array offset on value of non-string (samdark) +- Bug #17723: Fix incorrect decoding of default binary value for PostgreSQL (samdark) +- Bug #17723: Fix incorrect type-casting of reflection type to string (samdark) +- Bug #17725: Ensure we do not use external polyfills for pbkdf2() as these may be implemented incorrectly (samdark) +- Bug #17740: `yii\helpers\BaseInflector::slug()` doesn't replace multiple replacement string occurrences to single one (batyrmastyr) +- Bug #17745: Fix PostgreSQL query builder drops default value when it is empty (xepozz) +- Enh #17665: Implement RFC 7239 `Forwarded` header parsing in Request (mikk150, kamarton) +- Enh #17720: DI 3 support for application core components and default object configurations (sup-ham) + + +2.0.30 November 19, 2019 +------------------------ + +- Bug #17434: IE Ajax redirect fix for non 11.0 versions (kamarton) +- Bug #17632: Unicode file name was not correctly parsed in multipart forms (AlexRas007, samdark) +- Bug #17648: Handle empty column arrays in console `Table` widget (alex-code) +- Bug #17657: Fix migration errors from missing `$schema` in RBAC init file when using MSSQL (PoohOka) +- Bug #17670: Fix overriding core component class using `__class` (sup-ham, samdark) + + +2.0.29 October 22, 2019 +----------------------- + +- Bug #8225: Fixed AJAX validation with checkboxList was only triggered on first select (execut) +- Bug #17597: PostgreSQL 12 and partitioned tables support (batyrmastyr) +- Bug #17602: `EmailValidator` with `checkDNS=true` throws `ErrorException` on bad domains on Alpine (batyrmastyr) +- Bug #17606: Fix error in `AssetBundle` when a disabled bundle with custom init() was still published (onmotion) +- Bug #17625: Fix boolean `data` attributes from subkeys rendering in `Html::renderTagAttributes()` (brandonkelly) +- Enh #17607: Added Yii version 3 DI config compatibility (hiqsol) + + +2.0.28 October 08, 2019 +----------------------- + +- Bug #17573: `Request::getUserIP()` security fix for the case when `Request::$trustedHost` and `Request::$ipHeaders` are used (kamarton) +- Bug #17585: Fix `yii\i18n\Formatter` including the `@calendar` locale param in `Yii::t()` calls (brandonkelly) +- Bug #17853: Fix errors in ActiveField to be properly caught when PHP 7 is used (My6UoT9) + + +2.0.27 September 18, 2019 +------------------------- + +- Bug #16610: ErrorException trace was cut when using XDebug (Izumi-kun) +- Bug #16671: Logging in `Connection::open()` was not respecting `Connection::$enableLogging` (samdark) +- Bug #16855: Ignore console commands that have no actions (alexeevdv) +- Bug #17434: Fix regular expression illegal character; Repeated fix for Internet Explorer 11 AJAX redirect bug in case of 301 and 302 response codes (`XMLHttpRequest: Network Error 0x800c0008`) (kamarton) - Bug #17539: Fixed error when using `batch()` with `indexBy()` with MSSQL (alexkart) - Bug #17549: Fix `yii\db\ExpressionInterface` not supported in `yii\db\conditions\SimpleConditionBuilder` (razvanphp) -- Bug #17434: Fix regular expression illegal character; Repeated fix for Internet Explorer 11 AJAX redirect bug in case of 301 and 302 response codes (`XMLHttpRequest: Network Error 0x800c0008`) (kamarton) -- Bug #16855: Ignore console commands that have no actions (alexeevdv) +- Enh #15526: Show valid aliases and options on invalid input in console application (samdark) - Enh #16826: `appendTimestamp` support was added to `View` methods `registerCssFile()` and `registerJsFile()` (onmotion) + 2.0.26 September 03, 2019 ------------------------- @@ -1138,7 +1432,7 @@ Yii Framework 2 Change Log - Enh #8329: Added support of options for `message` console command (vchenin) - Enh #8613: `yii\widgets\FragmentCache` will not store empty content anymore which fixes some problems related to `yii\filters\PageCache` (kidol) - Enh #8649: Added total applied migrations to final report (vernik91) -- Enh #8687: Added support for non-gregorian calendars, e.g. persian, taiwan, islamic to `yii\i18n\Formatter` (cebe, z-avanes, hooman-pro) +- Enh #8687: Added support for non-gregorian calendars, e.g. persian, taiwan, islamic to `yii\i18n\Formatter` (cebe, z-avanes, hooman-mirghasemi) - Enh #8824: Allow passing a `yii\db\Expression` to `Query::groupBy()` (cebe) - Enh #8995: `yii\validators\FileValidator::maxFiles` can be set to `0` to allow unlimited count of files (PowerGamer1, silverfire) - Enh #9282: Improved JSON error handling to support PHP 5.5 error codes (freezy-sk) diff --git a/framework/LICENSE.md b/framework/LICENSE.md index e98f03d..ee872b9 100644 --- a/framework/LICENSE.md +++ b/framework/LICENSE.md @@ -1,6 +1,3 @@ -The Yii framework is free software. It is released under the terms of -the following BSD License. - Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) All rights reserved. diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index febb72a..02e3d28 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -51,6 +51,209 @@ if you want to upgrade from version A to version C and there is version B between A and C, you need to follow the instructions for both A and B. +Upgrade from Yii 2.0.39.3 +------------------------- + +* Priority of processing `yii\base\Arrayable`, and `JsonSerializable` data has been reversed (`Arrayable` data is checked + first now) in `yii\base\Model`, and `yii\rest\Serializer`. If your application relies on the previous priority you need + to fix it manually based on the complexity of desired (de)serialization result. + +Upgrade from Yii 2.0.38 +----------------------- + +* The storage structure of the file cache has been changed when you use `\yii\caching\FileCache::$keyPrefix`. +It is worth warming up the cache again if there is a logical dependency when working with the file cache. + +* `yii\web\Session` now respects the 'session.use_strict_mode' ini directive. + In case you use a custom `Session` class and have overwritten the `Session::openSession()` and/or + `Session::writeSession()` functions changes might be required: + * When in strict mode the `openSession()` function should check if the requested session id exists + (and mark it for forced regeneration if not). + For example, the `DbSession` does this at the beginning of the function as follows: + ```php + if ($this->getUseStrictMode()) { + $id = $this->getId(); + if (!$this->getReadQuery($id)->exists()) { + //This session id does not exist, mark it for forced regeneration + $this->_forceRegenerateId = $id; + } + } + // ... normal function continues ... + ``` + * When in strict mode the `writeSession()` function should ignore writing the session under the old id. + For example, the `DbSession` does this at the beginning of the function as follows: + ```php + if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) { + //Ignore write when forceRegenerate is active for this id + return true; + } + // ... normal function continues ... + ``` + > Note: The sample code above is specific for the `yii\web\DbSession` class. + Make sure you use the correct implementation based on your parent class, + e.g. `yii\web\CacheSession`, `yii\redis\Session`, `yii\mongodb\Session`, etc. + + > Note: In case your custom functions call their `parent` functions, there are probably no changes needed to your + code if those parents implement the `useStrictMode` checks. + + > Warning: in case `openSession()` and/or `writeSession()` functions do not implement the `useStrictMode` code + the session could be stored under a malicious id without warning even if `useStrictMode` is enabled. + +Upgrade from Yii 2.0.37 +----------------------- + +* Resolving DI references inside of arrays in dependencies was made optional and turned off by default. In order + to turn it on, set `resolveArrays` of container instance to `true`. + +Upgrade from Yii 2.0.36 +----------------------- + +* `yii\db\Exception::getCode()` now returns full PDO code that is SQLSTATE string. If you have relied on comparing code + with an integer value, adjust your code. + +Upgrade from Yii 2.0.35 +----------------------- + +* Inline validator signature has been updated with 4th parameter `current`: + + ```php + /** + * @param mixed $current the currently validated value of attribute + */ + function ($attribute, $params, $validator, $current) + ``` + +* Behavior of inline validator used as a rule of `EachValidator` has been changed - `$attribute` now refers to original + model's attribute and not its temporary counterpart: + + ```php + public $array_attribute = ['first', 'second']; + + public function rules() + { + return [ + ['array_attribute', 'each', 'rule' => ['customValidatingMethod']], + ]; + } + + public function customValidatingMethod($attribute, $params, $validator, $current) + { + // $attribute === 'array_attribute' (as before) + + // now: $this->$attribute === ['first', 'second'] (on every iteration) + // previously: + // $this->$attribute === 'first' (on first iteration) + // $this->$attribute === 'second' (on second iteration) + + // use now $current instead + // $current === 'first' (on first iteration) + // $current === 'second' (on second iteration) + } + ``` + +* `$this` in an inline validator defined as closure now refers to model instance. If you need to access the object registering + the validator, pass its instance through use statement: + + ```php + $registrar = $this; + $validator = function($attribute, $params, $validator, $current) use ($registrar) { + // ... + } + ``` + +* Validator closure callbacks should not be declared as static. + +* If you have any controllers that override the `init()` method, make sure they are calling `parent::init()` at + the beginning, as demonstrated in the [component guide](https://www.yiiframework.com/doc/guide/2.0/en/concept-components). + +Upgrade from Yii 2.0.34 +----------------------- + +* `ExistValidator` used as a rule of `EachValidator` now requires providing `targetClass` explicitely and it's not possible to use it with `targetRelation` in + that configuration. + + ```php + public function rules() + { + return [ + ['attribute', 'each', 'rule' => ['exist', 'targetClass' => static::className(), 'targetAttribute' => 'id']], + ]; + } + ``` + +Upgrade from Yii 2.0.32 +----------------------- + +* `yii\helpers\ArrayHelper::filter` now correctly filters data when passing a filter with more than 2 "levels", + e.g. `ArrayHelper::filter($myArray, ['A.B.C']`. Until Yii 2.0.32 all data after the 2nd level was returned, + please see the following example: + + ```php + $myArray = [ + 'A' => 1, + 'B' => [ + 'C' => 1, + 'D' => [ + 'E' => 1, + 'F' => 2, + ] + ], + ]; + ArrayHelper::filter($myArray, ['B.D.E']); + ``` + + Before Yii 2.0.33 this would return + + ```php + [ + 'B' => [ + 'D' => [ + 'E' => 1, + 'F' => 2, //Please note the unexpected inclusion of other elements + ], + ], + ] + ``` + + Since Yii 2.0.33 this returns + + ```php + [ + 'B' => [ + 'D' => [ + 'E' => 1, + ], + ], + ] + ``` + + Note: If you are only using up to 2 "levels" (e.g. `ArrayHelper::filter($myArray, ['A.B']`), this change has no impact. + +* `UploadedFile` class `deleteTempFile()` and `isUploadedFile()` methods introduced in 2.0.32 were removed. + +* Exception will be thrown if `UrlManager::$cache` configuration is incorrect (previously misconfiguration was silently + ignored and `UrlManager` continue to work without cache). Make sure that `UrlManager::$cache` is correctly configured + or set it to `null` to explicitly disable cache. + +Upgrade from Yii 2.0.31 +----------------------- + +* `yii\filters\ContentNegotiator` now generates 406 'Not Acceptable' instead of 415 'Unsupported Media Type' on + content-type negotiation fail. + +Upgrade from Yii 2.0.30 +----------------------- +* `yii\helpers\BaseInflector::slug()` now ensures there is no repeating $replacement string occurrences. + In case you rely on Yii 2.0.16 - 2.0.30 behavior, consider replacing `Inflector` with your own implementation. + + +Upgrade from Yii 2.0.28 +----------------------- + +* `yii\helpers\Html::tag()` now generates boolean attributes + [according to HTML specification](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attribute). + For `true` value attribute is present, for `false` value it is absent. + Upgrade from Yii 2.0.20 ----------------------- @@ -211,7 +414,10 @@ Upgrade from Yii 2.0.13 - If you are using XCache or Zend data cache, those are going away in 2.1 so you might want to start looking for an alternative. * In case you aren't using CSRF cookies (REST APIs etc.) you should turn them off explicitly by setting - `\yii\web\Request::$enableCsrfCookie` to `false` in your config file. + `\yii\web\Request::$enableCsrfCookie` to `false` in your config file. + +* Previously headers sent after content output was started were silently ignored. This behavior was changed to throwing + `\yii\web\HeadersAlreadySentException`. Upgrade from Yii 2.0.12 ----------------------- diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index d4dd15d..34d3346 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -14,11 +14,13 @@ $.fn.yiiActiveForm = function (method) { if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } else if (typeof method === 'object' || !method) { - return methods.init.apply(this, arguments); } else { - $.error('Method ' + method + ' does not exist on jQuery.yiiActiveForm'); - return false; + if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.yiiActiveForm'); + return false; + } } }; @@ -178,14 +180,14 @@ var submitDefer; - var setSubmitFinalizeDefer = function($form) { + var setSubmitFinalizeDefer = function ($form) { submitDefer = $.Deferred(); $form.data('yiiSubmitFinalizePromise', submitDefer.promise()); }; // finalize yii.js $form.submit - var submitFinalize = function($form) { - if(submitDefer) { + var submitFinalize = function ($form) { + if (submitDefer) { submitDefer.resolve(); submitDefer = undefined; $form.removeData('yiiSubmitFinalizePromise'); @@ -327,15 +329,22 @@ this.$form = $form; var $input = findInput($form, this); - if ($input.is(":disabled")) { + if ($input.is(':disabled')) { return true; } - // pass SELECT without options + // validate markup for select input if ($input.length && $input[0].tagName.toLowerCase() === 'select') { - if (!$input[0].options.length) { - return true; - } else if (($input[0].options.length === 1) && ($input[0].options[0].value === '')) { - return true; + var opts = $input[0].options, isEmpty = !opts || !opts.length, isRequired = $input.attr('required'), + isMultiple = $input.attr('multiple'), size = $input.attr('size') || 1; + // check if valid HTML markup for select input, else return validation as `true` + // https://w3c.github.io/html-reference/select.html + if (isRequired && !isMultiple && parseInt(size, 10) === 1) { // invalid select markup condition + if (isEmpty) { // empty option elements for the select + return true; + } + if (opts[0] && (opts[0].value !== '' && opts[0].text !== '')) { // first option is not empty + return true; + } } } this.cancelled = false; @@ -363,7 +372,7 @@ }); // ajax validation - $.when.apply(this, deferreds).always(function() { + $.when.apply(this, deferreds).always(function () { // Remove empty message arrays for (var i in messages) { if (0 === messages[i].length) { @@ -404,13 +413,15 @@ submitFinalize($form); } }); - } else if (data.submitting) { - // delay callback so that the form can be submitted without problem - window.setTimeout(function () { - updateInputs($form, messages, submitting); - }, 200); } else { - updateInputs($form, messages, submitting); + if (data.submitting) { + // delay callback so that the form can be submitted without problem + window.setTimeout(function () { + updateInputs($form, messages, submitting); + }, 200); + } else { + updateInputs($form, messages, submitting); + } } }); }, @@ -459,9 +470,9 @@ $errorElement = data.settings.validationStateOn === 'input' ? $input : $container; $errorElement.removeClass( - data.settings.validatingCssClass + ' ' + - data.settings.errorCssClass + ' ' + - data.settings.successCssClass + data.settings.validatingCssClass + ' ' + + data.settings.errorCssClass + ' ' + + data.settings.successCssClass ); $container.find(this.error).html(''); }); @@ -492,7 +503,7 @@ * @param id attribute ID * @param messages array with error messages */ - updateAttribute: function(id, messages) { + updateAttribute: function (id, messages) { var attribute = methods.find.call(this, id); if (attribute != undefined) { var msg = {}; @@ -518,7 +529,7 @@ } if (attribute.validateOnType) { $input.on('keyup.yiiActiveForm', function (e) { - if ($.inArray(e.which, [16, 17, 18, 37, 38, 39, 40]) !== -1 ) { + if ($.inArray(e.which, [16, 17, 18, 37, 38, 39, 40]) !== -1) { return; } if (attribute.value !== getValue($form, attribute)) { @@ -558,7 +569,13 @@ $.each(data.attributes, function () { if (this.status === 2) { this.status = 3; - $form.find(this.container).addClass(data.settings.validatingCssClass); + + var $container = $form.find(this.container), + $input = findInput($form, this); + + var $errorElement = data.settings.validationStateOn === 'input' ? $input : $container; + + $errorElement.addClass(data.settings.validatingCssClass); } }); methods.validate.call($form); @@ -571,7 +588,7 @@ * @param val2 * @returns boolean */ - var isEqual = function(val1, val2) { + var isEqual = function (val1, val2) { // objects if (val1 instanceof Object) { return isObjectsEqual(val1, val2) @@ -592,7 +609,7 @@ * @param obj2 * @returns boolean */ - var isObjectsEqual = function(obj1, obj2) { + var isObjectsEqual = function (obj1, obj2) { if (!(obj1 instanceof Object) || !(obj2 instanceof Object)) { return false; } @@ -621,7 +638,7 @@ * @param arr2 * @returns boolean */ - var isArraysEqual = function(arr1, arr2) { + var isArraysEqual = function (arr1, arr2) { if (!Array.isArray(arr1) || !Array.isArray(arr2)) { return false; } @@ -644,7 +661,7 @@ */ var deferredArray = function () { var array = []; - array.add = function(callback) { + array.add = function (callback) { this.push(new $.Deferred(callback)); }; return array; @@ -707,10 +724,11 @@ var errorAttributes = [], $input; $.each(data.attributes, function () { - var hasError = (submitting && updateInput($form, this, messages)) || (!submitting && attrHasError($form, this, messages)); + var hasError = (submitting && updateInput($form, this, messages)) || (!submitting && attrHasError($form, + this, messages)); $input = findInput($form, this); - if (!$input.is(":disabled") && !this.cancelled && hasError) { + if (!$input.is(':disabled') && !this.cancelled && hasError) { errorAttributes.push(this); } }); @@ -721,14 +739,10 @@ updateSummary($form, messages); if (errorAttributes.length) { if (data.settings.scrollToError) { - var top = $form.find($.map(errorAttributes, function(attribute) { + var h = $(document).height(), top = $form.find($.map(errorAttributes, function (attribute) { return attribute.input; }).join(',')).first().closest(':visible').offset().top - data.settings.scrollToErrorOffset; - if (top < 0) { - top = 0; - } else if (top > $(document).height()) { - top = $(document).height(); - } + top = top < 0 ? 0 : (top > h ? h : top); var wtop = $(window).scrollTop(); if (top < wtop || top > wtop + $(window).height()) { $(window).scrollTop(top); @@ -809,11 +823,11 @@ $error.html(messages[attribute.id][0]); } $errorElement.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass) - .addClass(data.settings.errorCssClass); + .addClass(data.settings.errorCssClass); } else { $error.empty(); $errorElement.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ') - .addClass(data.settings.successCssClass); + .addClass(data.settings.successCssClass); } attribute.value = getValue($form, attribute); } @@ -876,6 +890,14 @@ var type = $input.attr('type'); if (type === 'checkbox' || type === 'radio') { var $realInput = $input.filter(':checked'); + if ($realInput.length > 1) { + var values = []; + $realInput.each(function (index) { + values.push($($realInput.get(index)).val()); + }); + return values; + } + if (!$realInput.length) { $realInput = $form.find('input[type=hidden][name="' + $input.attr('name') + '"]'); } diff --git a/framework/assets/yii.js b/framework/assets/yii.js index 766877d..b22c364 100644 --- a/framework/assets/yii.js +++ b/framework/assets/yii.js @@ -297,7 +297,7 @@ window.yii = (function ($) { for (var i = 0, len = pairs.length; i < len; i++) { var pair = pairs[i].split('='); var name = decodeURIComponent(pair[0].replace(/\+/g, '%20')); - var value = decodeURIComponent(pair[1].replace(/\+/g, '%20')); + var value = pair.length > 1 ? decodeURIComponent(pair[1].replace(/\+/g, '%20')) : ''; if (!name.length) { continue; } diff --git a/framework/assets/yii.validation.js b/framework/assets/yii.validation.js index f0118f4..88f79de 100644 --- a/framework/assets/yii.validation.js +++ b/framework/assets/yii.validation.js @@ -408,10 +408,18 @@ yii.validation = (function ($) { function validateFile(file, messages, options) { if (options.extensions && options.extensions.length > 0) { - var index = file.name.lastIndexOf('.'); - var ext = !~index ? '' : file.name.substr(index + 1, file.name.length).toLowerCase(); + var found = false; + var filename = file.name.toLowerCase(); - if (!~options.extensions.indexOf(ext)) { + for (var index=0; index < options.extensions.length; index++) { + var ext = options.extensions[index].toLowerCase(); + if ((ext === '' && filename.indexOf('.') === -1) || (filename.substr(filename.length - options.extensions[index].length - 1) === ('.' + ext))) { + found = true; + break; + } + } + + if (!found) { messages.push(options.wrongExtension.replace(/\{file\}/g, file.name)); } } diff --git a/framework/base/Action.php b/framework/base/Action.php index bf6464e..5a9413c 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -30,7 +30,7 @@ use Yii; * * For more details and usage information on Action, see the [guide article on actions](guide:structure-controllers). * - * @property string $uniqueId The unique ID of this action among the whole application. This property is + * @property-read string $uniqueId The unique ID of this action among the whole application. This property is * read-only. * * @author Qiang Xue diff --git a/framework/base/Application.php b/framework/base/Application.php index d7771bf..94f4be1 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -14,33 +14,39 @@ use Yii; * * For more details and usage information on Application, see the [guide article on applications](guide:structure-applications). * - * @property \yii\web\AssetManager $assetManager The asset manager application component. This property is - * read-only. - * @property \yii\rbac\ManagerInterface $authManager The auth manager application component. Null is returned - * if auth manager is not configured. This property is read-only. + * @property-read \yii\web\AssetManager $assetManager The asset manager application component. This property + * is read-only. + * @property-read \yii\rbac\ManagerInterface $authManager The auth manager application component. Null is + * returned if auth manager is not configured. This property is read-only. * @property string $basePath The root directory of the application. - * @property \yii\caching\CacheInterface $cache The cache application component. Null if the component is not - * enabled. This property is read-only. - * @property array $container Values given in terms of name-value pairs. This property is write-only. - * @property \yii\db\Connection $db The database connection. This property is read-only. - * @property \yii\web\ErrorHandler|\yii\console\ErrorHandler $errorHandler The error handler application + * @property-read \yii\caching\CacheInterface $cache The cache application component. Null if the component is + * not enabled. This property is read-only. + * @property-write array $container Values given in terms of name-value pairs. This property is write-only. + * @property-read \yii\db\Connection $db The database connection. This property is read-only. + * @property-read \yii\web\ErrorHandler|\yii\console\ErrorHandler $errorHandler The error handler application * component. This property is read-only. - * @property \yii\i18n\Formatter $formatter The formatter application component. This property is read-only. - * @property \yii\i18n\I18N $i18n The internationalization application component. This property is read-only. - * @property \yii\log\Dispatcher $log The log dispatcher application component. This property is read-only. - * @property \yii\mail\MailerInterface $mailer The mailer application component. This property is read-only. - * @property \yii\web\Request|\yii\console\Request $request The request component. This property is read-only. - * @property \yii\web\Response|\yii\console\Response $response The response component. This property is + * @property-read \yii\i18n\Formatter $formatter The formatter application component. This property is + * read-only. + * @property-read \yii\i18n\I18N $i18n The internationalization application component. This property is + * read-only. + * @property-read \yii\log\Dispatcher $log The log dispatcher application component. This property is + * read-only. + * @property-read \yii\mail\MailerInterface $mailer The mailer application component. This property is + * read-only. + * @property-read \yii\web\Request|\yii\console\Request $request The request component. This property is + * read-only. + * @property-read \yii\web\Response|\yii\console\Response $response The response component. This property is * read-only. * @property string $runtimePath The directory that stores runtime files. Defaults to the "runtime" * subdirectory under [[basePath]]. - * @property \yii\base\Security $security The security application component. This property is read-only. + * @property-read \yii\base\Security $security The security application component. This property is read-only. * @property string $timeZone The time zone used by this application. - * @property string $uniqueId The unique ID of the module. This property is read-only. - * @property \yii\web\UrlManager $urlManager The URL manager for this application. This property is read-only. + * @property-read string $uniqueId The unique ID of the module. This property is read-only. + * @property-read \yii\web\UrlManager $urlManager The URL manager for this application. This property is + * read-only. * @property string $vendorPath The directory that stores vendor files. Defaults to "vendor" directory under * [[basePath]]. - * @property View|\yii\web\View $view The view application component that is used to render various view + * @property-read View|\yii\web\View $view The view application component that is used to render various view * files. This property is read-only. * * @author Qiang Xue diff --git a/framework/base/ArrayableTrait.php b/framework/base/ArrayableTrait.php index 2da307f..d2093fd 100644 --- a/framework/base/ArrayableTrait.php +++ b/framework/base/ArrayableTrait.php @@ -103,7 +103,7 @@ trait ArrayableTrait * This method will first identify which fields to be included in the resulting array by calling [[resolveFields()]]. * It will then turn the model into an array with these fields. If `$recursive` is true, * any embedded objects will also be converted into arrays. - * When embeded objects are [[Arrayable]], their respective nested fields will be extracted and passed to [[toArray()]]. + * When embedded objects are [[Arrayable]], their respective nested fields will be extracted and passed to [[toArray()]]. * * If the model implements the [[Linkable]] interface, the resulting array will also have a `_link` element * which refers to a list of links as specified by the interface. @@ -130,11 +130,15 @@ trait ArrayableTrait $nestedExpand = $this->extractFieldsFor($expand, $field); if ($attribute instanceof Arrayable) { $attribute = $attribute->toArray($nestedFields, $nestedExpand); + } elseif ($attribute instanceof \JsonSerializable) { + $attribute = $attribute->jsonSerialize(); } elseif (is_array($attribute)) { $attribute = array_map( function ($item) use ($nestedFields, $nestedExpand) { if ($item instanceof Arrayable) { return $item->toArray($nestedFields, $nestedExpand); + } elseif ($item instanceof \JsonSerializable) { + return $item->jsonSerialize(); } return $item; }, diff --git a/framework/base/Component.php b/framework/base/Component.php index 89bd36a..47e872b 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -93,7 +93,8 @@ use yii\helpers\StringHelper; * * For more details and usage information on Component, see the [guide article on components](guide:concept-components). * - * @property Behavior[] $behaviors List of behaviors attached to this component. This property is read-only. + * @property-read Behavior[] $behaviors List of behaviors attached to this component. This property is + * read-only. * * @author Qiang Xue * @since 2.0 @@ -567,7 +568,7 @@ class Component extends BaseObject } if ($removed) { $this->_events[$name] = array_values($this->_events[$name]); - return $removed; + return true; } } diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 9921cfe..f2d655b 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -8,18 +8,20 @@ namespace yii\base; use Yii; +use yii\di\Instance; +use yii\di\NotInstantiableException; /** * Controller is the base class for classes containing controller logic. * * For more details and usage information on Controller, see the [guide article on controllers](guide:structure-controllers). * - * @property Module[] $modules All ancestor modules that this controller is located within. This property is - * read-only. - * @property string $route The route (module ID, controller ID and action ID) of the current request. This + * @property-read Module[] $modules All ancestor modules that this controller is located within. This property + * is read-only. + * @property-read string $route The route (module ID, controller ID and action ID) of the current request. + * This property is read-only. + * @property-read string $uniqueId The controller ID that is prefixed with the module ID (if any). This * property is read-only. - * @property string $uniqueId The controller ID that is prefixed with the module ID (if any). This property is - * read-only. * @property View|\yii\web\View $view The view object that can be used to render views or view files. * @property string $viewPath The directory containing the view files for this controller. * @@ -59,17 +61,27 @@ class Controller extends Component implements ViewContextInterface */ public $layout; /** - * @var Action the action that is currently being executed. This property will be set + * @var Action|null the action that is currently being executed. This property will be set * by [[run()]] when it is called by [[Application]] to run an action. */ public $action; + /** + * @var Request|array|string The request. + * @since 2.0.36 + */ + public $request = 'request'; + /** + * @var Response|array|string The response. + * @since 2.0.36 + */ + public $response = 'response'; /** - * @var View the view object that can be used to render views or view files. + * @var View|null the view object that can be used to render views or view files. */ private $_view; /** - * @var string the root directory that contains view files for this controller. + * @var string|null the root directory that contains view files for this controller. */ private $_viewPath; @@ -87,6 +99,17 @@ class Controller extends Component implements ViewContextInterface } /** + * {@inheritdoc} + * @since 2.0.36 + */ + public function init() + { + parent::init(); + $this->request = Instance::ensure($this->request, Request::className()); + $this->response = Instance::ensure($this->response, Response::className()); + } + + /** * Declares external actions for the controller. * * This method is meant to be overwritten to declare external actions for the controller. @@ -106,6 +129,7 @@ class Controller extends Component implements ViewContextInterface * * [[\Yii::createObject()]] will be used later to create the requested action * using the configuration provided here. + * @return array */ public function actions() { @@ -523,4 +547,37 @@ class Controller extends Component implements ViewContextInterface return $path; } + + /** + * Fills parameters based on types and names in action method signature. + * @param \ReflectionType $type The reflected type of the action parameter. + * @param string $name The name of the parameter. + * @param array &$args The array of arguments for the action, this function may append items to it. + * @param array &$requestedParams The array with requested params, this function may write specific keys to it. + * @throws ErrorException when we cannot load a required service. + * @throws InvalidConfigException Thrown when there is an error in the DI configuration. + * @throws NotInstantiableException Thrown when a definition cannot be resolved to a concrete class + * (for example an interface type hint) without a proper definition in the container. + * @since 2.0.36 + */ + final protected function bindInjectedParams(\ReflectionType $type, $name, &$args, &$requestedParams) + { + // Since it is not a builtin type it must be DI injection. + $typeName = $type->getName(); + if (($component = $this->module->get($name, false)) instanceof $typeName) { + $args[] = $component; + $requestedParams[$name] = "Component: " . get_class($component) . " \$$name"; + } elseif ($this->module->has($typeName) && ($service = $this->module->get($typeName)) instanceof $typeName) { + $args[] = $service; + $requestedParams[$name] = 'Module ' . get_class($this->module) . " DI: $typeName \$$name"; + } elseif (\Yii::$container->has($typeName) && ($service = \Yii::$container->get($typeName)) instanceof $typeName) { + $args[] = $service; + $requestedParams[$name] = "Container DI: $typeName \$$name"; + } elseif ($type->allowsNull()) { + $args[] = null; + $requestedParams[$name] = "Unavailable service: $name"; + } else { + throw new Exception('Could not load required service: ' . $name); + } + } } diff --git a/framework/base/DynamicModel.php b/framework/base/DynamicModel.php index 7a872e1..610e03d 100644 --- a/framework/base/DynamicModel.php +++ b/framework/base/DynamicModel.php @@ -56,6 +56,16 @@ use yii\validators\Validator; class DynamicModel extends Model { private $_attributes = []; + /** + * Array of the dynamic attribute labels. + * Used to as form field labels and in validation errors. + * + * @see attributeLabels() + * @see setAttributeLabels() + * @see setAttributeLabel() + * @since 2.0.35 + */ + private $_attributeLabels = []; /** @@ -174,15 +184,26 @@ class DynamicModel extends Model * You can also directly manipulate [[validators]] to add or remove validation rules. * This method provides a shortcut. * @param string|array $attributes the attribute(s) to be validated by the rule - * @param mixed $validator the validator for the rule.This can be a built-in validator name, - * a method name of the model class, an anonymous function, or a validator class name. + * @param string|Validator|\Closure $validator the validator. This can be either: + * * a built-in validator name listed in [[builtInValidators]]; + * * a method name of the model class; + * * an anonymous function; + * * a validator class name. + * * a Validator. * @param array $options the options (name-value pairs) to be applied to the validator * @return $this the model itself */ public function addRule($attributes, $validator, $options = []) { $validators = $this->getValidators(); - $validators->append(Validator::createValidator($validator, $this, (array)$attributes, $options)); + + if ($validator instanceof Validator) { + $validator->attributes = (array)$attributes; + } else { + $validator = Validator::createValidator($validator, $this, (array)$attributes, $options); + } + + $validators->append($validator); return $this; } @@ -226,4 +247,47 @@ class DynamicModel extends Model { return array_keys($this->_attributes); } + + /** + * Sets the attribute labels in a massive way. + * + * @see attributeLabels() + * @see $_attributeLabels + * @since 2.0.35 + * + * @param array $labels Array of attribute labels + * @return $this + */ + public function setAttributeLabels(array $labels = []) + { + $this->_attributeLabels = $labels; + + return $this; + } + + /** + * Sets a label for an attribute. + * + * @see attributeLabels() + * @see $_attributeLabels + * @since 2.0.35 + * + * @param string $attribute Attribute name + * @param string $label Attribute label value + * @return $this + */ + public function setAttributeLabel($attribute, $label) + { + $this->_attributeLabels[$attribute] = $label; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function attributeLabels() + { + return array_merge(parent::attributeLabels(), $this->_attributeLabels); + } } diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php index ec64df7..03b662f 100644 --- a/framework/base/ErrorException.php +++ b/framework/base/ErrorException.php @@ -33,35 +33,35 @@ class ErrorException extends \ErrorException /** * Constructs the exception. * @link https://secure.php.net/manual/en/errorexception.construct.php - * @param $message [optional] - * @param $code [optional] - * @param $severity [optional] - * @param $filename [optional] - * @param $lineno [optional] - * @param $previous [optional] + * @param string $message [optional] + * @param int $code [optional] + * @param int $severity [optional] + * @param string $filename [optional] + * @param int $lineno [optional] + * @param \Throwable|\Exception $previous [optional] */ - public function __construct($message = '', $code = 0, $severity = 1, $filename = __FILE__, $lineno = __LINE__, \Exception $previous = null) + public function __construct($message = '', $code = 0, $severity = 1, $filename = __FILE__, $lineno = __LINE__, $previous = null) { parent::__construct($message, $code, $severity, $filename, $lineno, $previous); - if (function_exists('xdebug_get_function_stack')) { - // XDebug trace can't be modified and used directly with PHP 7 + if ($this->isXdebugStackAvailable()) { + // Xdebug trace can't be modified and used directly with PHP 7 // @see https://github.com/yiisoft/yii2/pull/11723 - $xDebugTrace = array_slice(array_reverse(xdebug_get_function_stack()), 3, -1); + $xdebugTrace = array_slice(array_reverse(xdebug_get_function_stack()), 1, -1); $trace = []; - foreach ($xDebugTrace as $frame) { + foreach ($xdebugTrace as $frame) { if (!isset($frame['function'])) { $frame['function'] = 'unknown'; } - // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 + // Xdebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 if (!isset($frame['type']) || $frame['type'] === 'static') { $frame['type'] = '::'; } elseif ($frame['type'] === 'dynamic') { $frame['type'] = '->'; } - // XDebug has a different key name + // Xdebug has a different key name if (isset($frame['params']) && !isset($frame['args'])) { $frame['args'] = $frame['params']; } @@ -75,6 +75,32 @@ class ErrorException extends \ErrorException } /** + * Ensures that Xdebug stack trace is available based on Xdebug version. + * Idea taken from developer bishopb at https://github.com/rollbar/rollbar-php + * @return bool + */ + private function isXdebugStackAvailable() + { + if (!function_exists('xdebug_get_function_stack')) { + return false; + } + + // check for Xdebug being installed to ensure origin of xdebug_get_function_stack() + $version = phpversion('xdebug'); + if ($version === false) { + return false; + } + + // Xdebug 2 and prior + if (version_compare($version, '3.0.0', '<')) { + return true; + } + + // Xdebug 3 and later, proper mode is required + return false !== strpos(ini_get('xdebug.mode'), 'develop'); + } + + /** * Returns if error is one of fatal type. * * @param array $error error got from error_get_last() diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 7f1afa8..8d98daf 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -41,6 +41,12 @@ abstract class ErrorHandler extends Component * @var \Exception|null the exception that is being handled currently. */ public $exception; + /** + * @var bool if true - `handleException()` will finish script with `ExitCode::OK`. + * false - `ExitCode::UNSPECIFIED_ERROR`. + * @since 2.0.36 + */ + public $silentExitOnException; /** * @var string Used to reserve memory for fatal error handler. @@ -50,33 +56,51 @@ abstract class ErrorHandler extends Component * @var \Exception from HHVM error that stores backtrace */ private $_hhvmException; + /** + * @var bool whether this instance has been registered using `register()` + */ + private $_registered = false; + public function init() + { + $this->silentExitOnException = $this->silentExitOnException !== null ? $this->silentExitOnException : YII_ENV_TEST; + parent::init(); + } + /** * Register this error handler. + * @since 2.0.32 this will not do anything if the error handler was already registered */ public function register() { - ini_set('display_errors', false); - set_exception_handler([$this, 'handleException']); - if (defined('HHVM_VERSION')) { - set_error_handler([$this, 'handleHhvmError']); - } else { - set_error_handler([$this, 'handleError']); - } - if ($this->memoryReserveSize > 0) { - $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize); + if (!$this->_registered) { + ini_set('display_errors', false); + set_exception_handler([$this, 'handleException']); + if (defined('HHVM_VERSION')) { + set_error_handler([$this, 'handleHhvmError']); + } else { + set_error_handler([$this, 'handleError']); + } + if ($this->memoryReserveSize > 0) { + $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize); + } + register_shutdown_function([$this, 'handleFatalError']); + $this->_registered = true; } - register_shutdown_function([$this, 'handleFatalError']); } /** * Unregisters this error handler by restoring the PHP error and exception handlers. + * @since 2.0.32 this will not do anything if the error handler was not registered */ public function unregister() { - restore_error_handler(); - restore_exception_handler(); + if ($this->_registered) { + restore_error_handler(); + restore_exception_handler(); + $this->_registered = false; + } } /** @@ -109,7 +133,7 @@ abstract class ErrorHandler extends Component $this->clearOutput(); } $this->renderException($exception); - if (!YII_ENV_TEST) { + if (!$this->silentExitOnException) { \Yii::getLogger()->flush(true); if (defined('HHVM_VERSION')) { flush(); @@ -212,16 +236,18 @@ abstract class ErrorHandler extends Component } $exception = new ErrorException($message, $code, $code, $file, $line); - // in case error appeared in __toString method we can't throw any exception - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - array_shift($trace); - foreach ($trace as $frame) { - if ($frame['function'] === '__toString') { - $this->handleException($exception); - if (defined('HHVM_VERSION')) { - flush(); + if (PHP_VERSION_ID < 70400) { + // prior to PHP 7.4 we can't throw exceptions inside of __toString() - it will result a fatal error + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + array_shift($trace); + foreach ($trace as $frame) { + if ($frame['function'] === '__toString') { + $this->handleException($exception); + if (defined('HHVM_VERSION')) { + flush(); + } + exit(1); } - exit(1); } } @@ -272,7 +298,7 @@ abstract class ErrorHandler extends Component /** * Renders the exception. - * @param \Exception $exception the exception to be rendered. + * @param \Exception|\Error|\Throwable $exception the exception to be rendered. */ abstract protected function renderException($exception); @@ -310,7 +336,7 @@ abstract class ErrorHandler extends Component * * This method can be used to convert exceptions inside of methods like `__toString()` * to PHP errors because exceptions cannot be thrown inside of them. - * @param \Exception $exception the exception to convert to a PHP error. + * @param \Exception|\Throwable $exception the exception to convert to a PHP error. */ public static function convertExceptionToError($exception) { @@ -319,7 +345,7 @@ abstract class ErrorHandler extends Component /** * Converts an exception into a simple string. - * @param \Exception|\Error $exception the exception being converted + * @param \Exception|\Error|\Throwable $exception the exception being converted * @return string the string representation of the exception. */ public static function convertExceptionToString($exception) @@ -337,7 +363,7 @@ abstract class ErrorHandler extends Component /** * Converts an exception into a string that has verbose information about the exception and its trace. - * @param \Exception|\Error $exception the exception being converted + * @param \Exception|\Error|\Throwable $exception the exception being converted * @return string the string representation of the exception. * * @since 2.0.14 diff --git a/framework/base/Event.php b/framework/base/Event.php index 9ffe178..29bc0a8 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -158,7 +158,7 @@ class Event extends BaseObject } if ($removed) { self::$_events[$name][$class] = array_values(self::$_events[$name][$class]); - return $removed; + return true; } } @@ -297,7 +297,7 @@ class Event extends BaseObject foreach ($classes as $class) { $eventHandlers = []; foreach ($wildcardEventHandlers as $classWildcard => $handlers) { - if (StringHelper::matchWildcard($classWildcard, $class)) { + if (StringHelper::matchWildcard($classWildcard, $class, ['escape' => false])) { $eventHandlers = array_merge($eventHandlers, $handlers); unset($wildcardEventHandlers[$classWildcard]); } diff --git a/framework/base/ExitException.php b/framework/base/ExitException.php index 9197b0f..4fde6a8 100644 --- a/framework/base/ExitException.php +++ b/framework/base/ExitException.php @@ -28,9 +28,9 @@ class ExitException extends \Exception * @param int $status the exit status code * @param string $message error message * @param int $code error code - * @param \Exception $previous The previous exception used for the exception chaining. + * @param \Throwable|\Exception $previous The previous exception used for the exception chaining. */ - public function __construct($status = 0, $message = null, $code = 0, \Exception $previous = null) + public function __construct($status = 0, $message = null, $code = 0, $previous = null) { $this->statusCode = $status; parent::__construct($message, $code, $previous); diff --git a/framework/base/Model.php b/framework/base/Model.php index 9dc9417..085ec1a 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -37,19 +37,20 @@ use yii\validators\Validator; * * For more details and usage information on Model, see the [guide article on models](guide:structure-models). * - * @property \yii\validators\Validator[] $activeValidators The validators applicable to the current + * @property-read \yii\validators\Validator[] $activeValidators The validators applicable to the current * [[scenario]]. This property is read-only. * @property array $attributes Attribute values (name => value). - * @property array $errors An array of errors for all attributes. Empty array is returned if no error. The - * result is a two-dimensional array. See [[getErrors()]] for detailed description. This property is read-only. - * @property array $firstErrors The first errors. The array keys are the attribute names, and the array values - * are the corresponding error messages. An empty array will be returned if there is no error. This property is + * @property-read array $errors An array of errors for all attributes. Empty array is returned if no error. + * The result is a two-dimensional array. See [[getErrors()]] for detailed description. This property is * read-only. - * @property ArrayIterator $iterator An iterator for traversing the items in the list. This property is + * @property-read array $firstErrors The first errors. The array keys are the attribute names, and the array + * values are the corresponding error messages. An empty array will be returned if there is no error. This + * property is read-only. + * @property-read ArrayIterator $iterator An iterator for traversing the items in the list. This property is * read-only. * @property string $scenario The scenario that this model is in. Defaults to [[SCENARIO_DEFAULT]]. - * @property ArrayObject|\yii\validators\Validator[] $validators All the validators declared in the model. - * This property is read-only. + * @property-read ArrayObject|\yii\validators\Validator[] $validators All the validators declared in the + * model. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -639,7 +640,7 @@ class Model extends Component implements StaticInstanceInterface, IteratorAggreg $lines = []; $errors = $showAllErrors ? $this->getErrors() : $this->getFirstErrors(); foreach ($errors as $es) { - $lines = array_merge((array)$es, $lines); + $lines = array_merge($lines, (array)$es); } return $lines; } @@ -798,7 +799,7 @@ class Model extends Component implements StaticInstanceInterface, IteratorAggreg } $attributes = []; foreach ($scenarios[$scenario] as $attribute) { - if ($attribute[0] !== '!' && !in_array('!' . $attribute, $scenarios[$scenario])) { + if (strncmp($attribute, '!', 1) !== 0 && !in_array('!' . $attribute, $scenarios[$scenario])) { $attributes[] = $attribute; } } @@ -819,7 +820,7 @@ class Model extends Component implements StaticInstanceInterface, IteratorAggreg } $attributes = array_keys(array_flip($scenarios[$scenario])); foreach ($attributes as $i => $attribute) { - if ($attribute[0] === '!') { + if (strncmp($attribute, '!', 1) === 0) { $attributes[$i] = substr($attribute, 1); } } diff --git a/framework/base/Module.php b/framework/base/Module.php index d595416..bf73e70 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -23,17 +23,17 @@ use yii\di\ServiceLocator; * * For more details and usage information on Module, see the [guide article on modules](guide:structure-modules). * - * @property array $aliases List of path aliases to be defined. The array keys are alias names (must start - * with `@`) and the array values are the corresponding paths or aliases. See [[setAliases()]] for an example. - * This property is write-only. + * @property-write array $aliases List of path aliases to be defined. The array keys are alias names (must + * start with `@`) and the array values are the corresponding paths or aliases. See [[setAliases()]] for an + * example. This property is write-only. * @property string $basePath The root directory of the module. - * @property string $controllerPath The directory that contains the controller classes. This property is + * @property-read string $controllerPath The directory that contains the controller classes. This property is * read-only. * @property string $layoutPath The root directory of layout files. Defaults to "[[viewPath]]/layouts". * @property array $modules The modules (indexed by their IDs). - * @property string $uniqueId The unique ID of the module. This property is read-only. + * @property-read string $uniqueId The unique ID of the module. This property is read-only. * @property string $version The version of this module. Note that the type of this property differs in getter - * and setter. See [[getVersion()]] and [[setVersion()]] for details. + * and setter. See [[getVersion()]] and [[setVersion()]] for details. * @property string $viewPath The root directory of view files. Defaults to "[[basePath]]/views". * * @author Qiang Xue @@ -60,11 +60,11 @@ class Module extends ServiceLocator */ public $id; /** - * @var Module the parent module of this module. `null` if this module does not have a parent. + * @var Module|null the parent module of this module. `null` if this module does not have a parent. */ public $module; /** - * @var string|bool the layout that should be applied for views within this module. This refers to a view name + * @var string|bool|null the layout that should be applied for views within this module. This refers to a view name * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] * will be taken. If this is `false`, layout will be disabled within this module. */ @@ -90,7 +90,7 @@ class Module extends ServiceLocator */ public $controllerMap = []; /** - * @var string the namespace that controller classes are in. + * @var string|null the namespace that controller classes are in. * This namespace will be used to load controller classes by prepending it to the controller * class name. * @@ -425,7 +425,7 @@ class Module extends ServiceLocator Yii::debug("Loading module: $id", __METHOD__); /* @var $module Module */ $module = Yii::createObject($this->_modules[$id], [$id, $this]); - $module->setInstance($module); + $module::setInstance($module); return $this->_modules[$id] = $module; } } @@ -450,6 +450,9 @@ class Module extends ServiceLocator unset($this->_modules[$id]); } else { $this->_modules[$id] = $module; + if ($module instanceof self) { + $module->module = $this; + } } } @@ -504,6 +507,9 @@ class Module extends ServiceLocator { foreach ($modules as $id => $module) { $this->_modules[$id] = $module; + if ($module instanceof self) { + $module->module = $this; + } } } diff --git a/framework/base/Security.php b/framework/base/Security.php index c4deb1e..e3a2373 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -91,6 +91,38 @@ class Security extends Component */ public $passwordHashCost = 13; + /** + * @var boolean if LibreSSL should be used. + * The recent (> 2.1.5) LibreSSL RNGs are faster and likely better than /dev/urandom. + */ + private $_useLibreSSL; + + + /** + * @return bool if LibreSSL should be used + * Use version is 2.1.5 or higher. + * @since 2.0.36 + */ + protected function shouldUseLibreSSL() + { + if ($this->_useLibreSSL === null) { + // Parse OPENSSL_VERSION_TEXT because OPENSSL_VERSION_NUMBER is no use for LibreSSL. + // https://bugs.php.net/bug.php?id=71143 + $this->_useLibreSSL = defined('OPENSSL_VERSION_TEXT') + && preg_match('{^LibreSSL (\d\d?)\.(\d\d?)\.(\d\d?)$}', OPENSSL_VERSION_TEXT, $matches) + && (10000 * $matches[1]) + (100 * $matches[2]) + $matches[3] >= 20105; + } + + return $this->_useLibreSSL; + } + + /** + * @return bool if operating system is Windows + */ + private function isWindows() + { + return DIRECTORY_SEPARATOR !== '/'; + } /** * Encrypts data using a password. @@ -330,7 +362,7 @@ class Security extends Component */ public function pbkdf2($algo, $password, $salt, $iterations, $length = 0) { - if (function_exists('hash_pbkdf2')) { + if (function_exists('hash_pbkdf2') && PHP_VERSION_ID >= 50500) { $outputKey = hash_pbkdf2($algo, $password, $salt, $iterations, $length, true); if ($outputKey === false) { throw new InvalidArgumentException('Invalid parameters to hash_pbkdf2()'); @@ -439,7 +471,6 @@ class Security extends Component return false; } - private $_useLibreSSL; private $_randomFile; /** @@ -468,22 +499,10 @@ class Security extends Component } // The recent LibreSSL RNGs are faster and likely better than /dev/urandom. - // Parse OPENSSL_VERSION_TEXT because OPENSSL_VERSION_NUMBER is no use for LibreSSL. - // https://bugs.php.net/bug.php?id=71143 - if ($this->_useLibreSSL === null) { - $this->_useLibreSSL = defined('OPENSSL_VERSION_TEXT') - && preg_match('{^LibreSSL (\d\d?)\.(\d\d?)\.(\d\d?)$}', OPENSSL_VERSION_TEXT, $matches) - && (10000 * $matches[1]) + (100 * $matches[2]) + $matches[3] >= 20105; - } - // Since 5.4.0, openssl_random_pseudo_bytes() reads from CryptGenRandom on Windows instead // of using OpenSSL library. LibreSSL is OK everywhere but don't use OpenSSL on non-Windows. if (function_exists('openssl_random_pseudo_bytes') - && ($this->_useLibreSSL - || ( - DIRECTORY_SEPARATOR !== '/' - && substr_compare(PHP_OS, 'win', 0, 3, true) === 0 - )) + && ($this->shouldUseLibreSSL() || $this->isWindows()) ) { $key = openssl_random_pseudo_bytes($length, $cryptoStrong); if ($cryptoStrong === false) { @@ -506,7 +525,7 @@ class Security extends Component } // If not on Windows, try to open a random device. - if ($this->_randomFile === null && DIRECTORY_SEPARATOR === '/') { + if ($this->_randomFile === null && !$this->isWindows()) { // urandom is a symlink to random on FreeBSD. $device = PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom'; // Check random device for special character device protection mode. Use lstat() diff --git a/framework/base/View.php b/framework/base/View.php index 73aac16..41b3d96 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -20,7 +20,7 @@ use yii\widgets\FragmentCache; * * For more details and usage information on View, see the [guide article on views](guide:structure-views). * - * @property string|bool $viewFile The view file currently being rendered. False if no view file is being + * @property-read string|bool $viewFile The view file currently being rendered. False if no view file is being * rendered. This property is read-only. * * @author Qiang Xue @@ -50,7 +50,7 @@ class View extends Component implements DynamicContentAwareInterface */ public $context; /** - * @var mixed custom parameters that are shared among view templates. + * @var array custom parameters that are shared among view templates. */ public $params = []; /** @@ -372,6 +372,11 @@ class View extends Component implements DynamicContentAwareInterface * @param string $statements the PHP statements for generating the dynamic content. * @return string the placeholder of the dynamic content, or the dynamic content if there is no * active content cache currently. + * + * Note that most methods that indirectly modify layout such as registerJS() or registerJSFile() do not + * work with dynamic rendering. + * + * @see https://github.com/yiisoft/yii2/issues/17673 */ public function renderDynamic($statements) { diff --git a/framework/base/Widget.php b/framework/base/Widget.php index d578ae4..ab27f02 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -15,10 +15,11 @@ use Yii; * * For more details and usage information on Widget, see the [guide article on widgets](guide:structure-widgets). * - * @property string $id ID of the widget. + * @property string|null $id ID of the widget. Note that the type of this property differs in getter and + * setter. See [[getId()]] and [[setId()]] for details. * @property \yii\web\View $view The view object that can be used to render views or view files. Note that the - * type of this property differs in getter and setter. See [[getView()]] and [[setView()]] for details. - * @property string $viewPath The directory containing the view files for this widget. This property is + * type of this property differs in getter and setter. See [[getView()]] and [[setView()]] for details. + * @property-read string $viewPath The directory containing the view files for this widget. This property is * read-only. * * @author Qiang Xue @@ -156,7 +157,7 @@ class Widget extends Component implements ViewContextInterface /** * Returns the ID of the widget. * @param bool $autoGenerate whether to generate an ID if it is not set previously - * @return string ID of the widget. + * @return string|null ID of the widget. */ public function getId($autoGenerate = true) { diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index 08f2920..b221998 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -197,24 +197,31 @@ class FileCache extends Cache } /** - * Returns the cache file path given the cache key. - * @param string $key cache key + * Returns the cache file path given the normalized cache key. + * @param string $normalizedKey normalized cache key by [[buildKey]] method * @return string the cache file path */ - protected function getCacheFile($key) + protected function getCacheFile($normalizedKey) { + $cacheKey = $normalizedKey; + + if ($this->keyPrefix !== '') { + // Remove key prefix to avoid generating constant directory levels + $lenKeyPrefix = strlen($this->keyPrefix); + $cacheKey = substr_replace($normalizedKey, '', 0, $lenKeyPrefix); + } + + $cachePath = $this->cachePath; + if ($this->directoryLevel > 0) { - $base = $this->cachePath; for ($i = 0; $i < $this->directoryLevel; ++$i) { - if (($prefix = substr($key, $i + $i, 2)) !== false) { - $base .= DIRECTORY_SEPARATOR . $prefix; + if (($subDirectory = substr($cacheKey, $i + $i, 2)) !== false) { + $cachePath .= DIRECTORY_SEPARATOR . $subDirectory; } } - - return $base . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; } - return $this->cachePath . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; + return $cachePath . DIRECTORY_SEPARATOR . $normalizedKey . $this->cacheFileSuffix; } /** @@ -254,7 +261,7 @@ class FileCache extends Cache { if (($handle = opendir($path)) !== false) { while (($file = readdir($handle)) !== false) { - if ($file[0] === '.') { + if (strpos($file, '.') === 0) { continue; } $fullPath = $path . DIRECTORY_SEPARATOR . $file; diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php index 8b43120..2a83e5d 100644 --- a/framework/caching/MemCache.php +++ b/framework/caching/MemCache.php @@ -55,10 +55,10 @@ use yii\base\InvalidConfigException; * * For more details and usage information on Cache, see the [guide article on caching](guide:caching-overview). * - * @property \Memcache|\Memcached $memcache The memcache (or memcached) object used by this cache component. - * This property is read-only. + * @property-read \Memcache|\Memcached $memcache The memcache (or memcached) object used by this cache + * component. This property is read-only. * @property MemCacheServer[] $servers List of memcache server configurations. Note that the type of this - * property differs in getter and setter. See [[getServers()]] and [[setServers()]] for details. + * property differs in getter and setter. See [[getServers()]] and [[setServers()]] for details. * * @author Qiang Xue * @since 2.0 @@ -292,11 +292,7 @@ class MemCache extends Cache */ protected function setValue($key, $value, $duration) { - // Use UNIX timestamp since it doesn't have any limitation - // @see https://secure.php.net/manual/en/memcache.set.php - // @see https://secure.php.net/manual/en/memcached.expiration.php - $expire = $duration > 0 ? $duration + time() : 0; - + $expire = $this->normalizeDuration($duration); return $this->useMemcached ? $this->_cache->set($key, $value, $expire) : $this->_cache->set($key, $value, 0, $expire); } @@ -309,10 +305,7 @@ class MemCache extends Cache protected function setValues($data, $duration) { if ($this->useMemcached) { - // Use UNIX timestamp since it doesn't have any limitation - // @see https://secure.php.net/manual/en/memcache.set.php - // @see https://secure.php.net/manual/en/memcached.expiration.php - $expire = $duration > 0 ? $duration + time() : 0; + $expire = $this->normalizeDuration($duration); // Memcached::setMulti() returns boolean // @see https://secure.php.net/manual/en/memcached.setmulti.php @@ -334,11 +327,7 @@ class MemCache extends Cache */ protected function addValue($key, $value, $duration) { - // Use UNIX timestamp since it doesn't have any limitation - // @see https://secure.php.net/manual/en/memcache.set.php - // @see https://secure.php.net/manual/en/memcached.expiration.php - $expire = $duration > 0 ? $duration + time() : 0; - + $expire = $this->normalizeDuration($duration); return $this->useMemcached ? $this->_cache->add($key, $value, $expire) : $this->_cache->add($key, $value, 0, $expire); } @@ -362,4 +351,28 @@ class MemCache extends Cache { return $this->_cache->flush(); } + + /** + * Normalizes duration value + * + * @see https://github.com/yiisoft/yii2/issues/17710 + * @see https://secure.php.net/manual/en/memcache.set.php + * @see https://secure.php.net/manual/en/memcached.expiration.php + * + * @since 2.0.31 + * @param int $duration + * @return int + */ + protected function normalizeDuration($duration) + { + if ($duration < 0) { + return 0; + } + + if ($duration < 2592001) { + return $duration; + } + + return $duration + time(); + } } diff --git a/framework/captcha/CaptchaAction.php b/framework/captcha/CaptchaAction.php index 3ded638..40e1aa0 100644 --- a/framework/captcha/CaptchaAction.php +++ b/framework/captcha/CaptchaAction.php @@ -31,7 +31,7 @@ use yii\web\Response; * to be validated by the 'captcha' validator. * 3. In the controller view, insert a [[Captcha]] widget in the form. * - * @property string $verifyCode The verification code. This property is read-only. + * @property-read string $verifyCode The verification code. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/classes.php b/framework/classes.php index db918ea..622a1b7 100644 --- a/framework/classes.php +++ b/framework/classes.php @@ -187,6 +187,7 @@ return [ 'yii\db\mysql\QueryBuilder' => YII2_PATH . '/db/mysql/QueryBuilder.php', 'yii\db\mysql\Schema' => YII2_PATH . '/db/mysql/Schema.php', 'yii\db\oci\ColumnSchemaBuilder' => YII2_PATH . '/db/oci/ColumnSchemaBuilder.php', + 'yii\db\oci\Command' => YII2_PATH . '/db/oci/Command.php', 'yii\db\oci\QueryBuilder' => YII2_PATH . '/db/oci/QueryBuilder.php', 'yii\db\oci\Schema' => YII2_PATH . '/db/oci/Schema.php', 'yii\db\oci\conditions\InConditionBuilder' => YII2_PATH . '/db/oci/conditions/InConditionBuilder.php', diff --git a/framework/composer.json b/framework/composer.json index 482d992..40bfe5d 100644 --- a/framework/composer.json +++ b/framework/composer.json @@ -70,7 +70,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.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.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" diff --git a/framework/console/Application.php b/framework/console/Application.php index 431198a..51d81e4 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -50,9 +50,10 @@ defined('STDERR') or define('STDERR', fopen('php://stderr', 'w')); * yii help * ``` * - * @property ErrorHandler $errorHandler The error handler application component. This property is read-only. - * @property Request $request The request component. This property is read-only. - * @property Response $response The response component. This property is read-only. + * @property-read ErrorHandler $errorHandler The error handler application component. This property is + * read-only. + * @property-read Request $request The request component. This property is read-only. + * @property-read Response $response The response component. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 02b7009..fa05553 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -28,12 +28,14 @@ use yii\helpers\Inflector; * where `` is a route to a controller action and the params will be populated as properties of a command. * See [[options()]] for details. * - * @property string $help This property is read-only. - * @property string $helpSummary This property is read-only. - * @property array $passedOptionValues The properties corresponding to the passed options. This property is - * read-only. - * @property array $passedOptions The names of the options passed during execution. This property is + * @property-read string $help This property is read-only. + * @property-read string $helpSummary This property is read-only. + * @property-read array $passedOptionValues The properties corresponding to the passed options. This property + * is read-only. + * @property-read array $passedOptions The names of the options passed during execution. This property is * read-only. + * @property Request $request + * @property Response $response * * @author Qiang Xue * @since 2.0 @@ -54,7 +56,7 @@ class Controller extends \yii\base\Controller */ public $interactive = true; /** - * @var bool whether to enable ANSI color in the output. + * @var bool|null whether to enable ANSI color in the output. * If not set, ANSI color will only be enabled for terminals that support it. */ public $color; @@ -62,7 +64,14 @@ class Controller extends \yii\base\Controller * @var bool whether to display help information about current command. * @since 2.0.10 */ - public $help; + public $help = false; + /** + * @var bool|null if true - script finish with `ExitCode::OK` in case of exception. + * false - `ExitCode::UNSPECIFIED_ERROR`. + * Default: `YII_ENV_TEST` + * @since 2.0.36 + */ + public $silentExitOnException; /** * @var array the options passed during execution. @@ -71,6 +80,17 @@ class Controller extends \yii\base\Controller /** + * {@inheritdoc} + */ + public function beforeAction($action) + { + $silentExit = $this->silentExitOnException !== null ? $this->silentExitOnException : YII_ENV_TEST; + Yii::$app->errorHandler->silentExitOnException = $silentExit; + + return parent::beforeAction($action); + } + + /** * Returns a value indicating whether ANSI color is enabled. * * ANSI color is enabled only if [[color]] is set true or is not set @@ -105,7 +125,18 @@ class Controller extends \yii\base\Controller if (array_key_exists($name, $optionAliases)) { $params[$optionAliases[$name]] = $value; } else { - throw new Exception(Yii::t('yii', 'Unknown alias: -{name}', ['name' => $name])); + $message = Yii::t('yii', 'Unknown alias: -{name}', ['name' => $name]); + if (!empty($optionAliases)) { + $aliasesAvailable = []; + foreach ($optionAliases as $alias => $option) { + $aliasesAvailable[] = '-' . $alias . ' (--' . $option . ')'; + } + + $message .= '. ' . Yii::t('yii', 'Aliases available: {aliases}', [ + 'aliases' => implode(', ', $aliasesAvailable) + ]); + } + throw new Exception($message); } } unset($params['_aliases']); @@ -122,7 +153,7 @@ class Controller extends \yii\base\Controller if (in_array($name, $options, true)) { $default = $this->$name; - if (is_array($default)) { + if (is_array($default) && is_string($value)) { $this->$name = preg_split('/\s*,\s*(?![^()]*\))/', $value); } elseif ($default !== null) { settype($value, gettype($default)); @@ -136,7 +167,12 @@ class Controller extends \yii\base\Controller unset($params[$kebabName]); } } elseif (!is_int($name)) { - throw new Exception(Yii::t('yii', 'Unknown option: --{name}', ['name' => $name])); + $message = Yii::t('yii', 'Unknown option: --{name}', ['name' => $name]); + if (!empty($options)) { + $message .= '. ' . Yii::t('yii', 'Options available: {options}', ['options' => '--' . implode(', --', $options)]); + } + + throw new Exception($message); } } } @@ -166,19 +202,40 @@ class Controller extends \yii\base\Controller $method = new \ReflectionMethod($action, 'run'); } - $args = array_values($params); - + $args = []; $missing = []; + $actionParams = []; + $requestedParams = []; foreach ($method->getParameters() as $i => $param) { - if ($param->isArray() && isset($args[$i])) { - $args[$i] = $args[$i] === '' ? [] : preg_split('/\s*,\s*/', $args[$i]); + $name = $param->getName(); + $key = null; + if (array_key_exists($i, $params)) { + $key = $i; + } elseif (array_key_exists($name, $params)) { + $key = $name; } - if (!isset($args[$i])) { - if ($param->isDefaultValueAvailable()) { - $args[$i] = $param->getDefaultValue(); + + if ($key !== null) { + if (PHP_VERSION_ID >= 80000) { + $isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array'; } else { - $missing[] = $param->getName(); + $isArray = $param->isArray(); + } + if ($isArray) { + $params[$key] = $params[$key] === '' ? [] : preg_split('/\s*,\s*/', $params[$key]); } + $args[] = $actionParams[$key] = $params[$key]; + unset($params[$key]); + } elseif (PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null && !$type->isBuiltin()) { + try { + $this->bindInjectedParams($type, $name, $args, $requestedParams); + } catch (\yii\base\Exception $e) { + throw new Exception($e->getMessage()); + } + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $actionParams[$i] = $param->getDefaultValue(); + } else { + $missing[] = $name; } } @@ -186,7 +243,12 @@ class Controller extends \yii\base\Controller throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)])); } - return $args; + // We use a different array here, specifically one that doesn't contain service instances but descriptions instead. + if (\Yii::$app->requestedParams === null) { + \Yii::$app->requestedParams = array_merge($actionParams, $requestedParams); + } + + return array_merge($args, $params); } /** @@ -227,6 +289,7 @@ class Controller extends \yii\base\Controller * ``` * * @param string $string the string to print + * @param int ...$args additional parameters to decorate the output * @return int|bool Number of bytes printed or false on error */ public function stdout($string) @@ -358,7 +421,7 @@ class Controller extends \yii\base\Controller public function options($actionID) { // $actionId might be used in subclasses to provide options specific to action id - return ['color', 'interactive', 'help']; + return ['color', 'interactive', 'help', 'silentExitOnException']; } /** @@ -497,7 +560,13 @@ class Controller extends \yii\base\Controller /** @var \ReflectionParameter $reflection */ foreach ($method->getParameters() as $i => $reflection) { - if ($reflection->getClass() !== null) { + if (PHP_VERSION_ID >= 80000) { + $class = $reflection->getType(); + } else { + $class = $reflection->getClass(); + } + + if ($class !== null) { continue; } $name = $reflection->getName(); diff --git a/framework/console/ErrorHandler.php b/framework/console/ErrorHandler.php index f40e879..16127a1 100644 --- a/framework/console/ErrorHandler.php +++ b/framework/console/ErrorHandler.php @@ -29,6 +29,7 @@ class ErrorHandler extends \yii\base\ErrorHandler */ protected function renderException($exception) { + $previous = $exception->getPrevious(); if ($exception instanceof UnknownCommandException) { // display message and suggest alternatives in case of unknown command $message = $this->formatMessage($exception->getName() . ': ') . $exception->command; @@ -55,7 +56,9 @@ class ErrorHandler extends \yii\base\ErrorHandler if ($exception instanceof \yii\db\Exception && !empty($exception->errorInfo)) { $message .= "\n" . $this->formatMessage("Error Info:\n", [Console::BOLD]) . print_r($exception->errorInfo, true); } - $message .= "\n" . $this->formatMessage("Stack trace:\n", [Console::BOLD]) . $exception->getTraceAsString(); + if ($previous === null) { + $message .= "\n" . $this->formatMessage("Stack trace:\n", [Console::BOLD]) . $exception->getTraceAsString(); + } } else { $message = $this->formatMessage('Error: ') . $exception->getMessage(); } @@ -65,6 +68,15 @@ class ErrorHandler extends \yii\base\ErrorHandler } else { echo $message . "\n"; } + if (YII_DEBUG && $previous !== null) { + $causedBy = $this->formatMessage('Caused by: ', [Console::BOLD]); + if (PHP_SAPI === 'cli') { + Console::stderr($causedBy); + } else { + echo $causedBy; + } + $this->renderException($previous); + } } /** diff --git a/framework/console/controllers/AssetController.php b/framework/console/controllers/AssetController.php index 4f2e492..fe106f7 100644 --- a/framework/console/controllers/AssetController.php +++ b/framework/console/controllers/AssetController.php @@ -39,7 +39,7 @@ use yii\web\AssetBundle; * check [[jsCompressor]] and [[cssCompressor]] for more details. * * @property \yii\web\AssetManager $assetManager Asset manager instance. Note that the type of this property - * differs in getter and setter. See [[getAssetManager()]] and [[setAssetManager()]] for details. + * differs in getter and setter. See [[getAssetManager()]] and [[setAssetManager()]] for details. * * @author Qiang Xue * @author Paul Klimov diff --git a/framework/console/controllers/BaseMigrateController.php b/framework/console/controllers/BaseMigrateController.php index 704bba7..dc94497 100644 --- a/framework/console/controllers/BaseMigrateController.php +++ b/framework/console/controllers/BaseMigrateController.php @@ -119,6 +119,8 @@ abstract class BaseMigrateController extends Controller throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.'); } + $this->migrationNamespaces = (array) $this->migrationNamespaces; + foreach ($this->migrationNamespaces as $key => $value) { $this->migrationNamespaces[$key] = trim($value, '\\'); } @@ -209,6 +211,8 @@ abstract class BaseMigrateController extends Controller $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " applied.\n", Console::FG_GREEN); $this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN); } + + return ExitCode::OK; } /** @@ -270,6 +274,8 @@ abstract class BaseMigrateController extends Controller $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " reverted.\n", Console::FG_GREEN); $this->stdout("\nMigrated down successfully.\n", Console::FG_GREEN); } + + return ExitCode::OK; } /** @@ -336,6 +342,8 @@ abstract class BaseMigrateController extends Controller $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " redone.\n", Console::FG_GREEN); $this->stdout("\nMigration redone successfully.\n", Console::FG_GREEN); } + + return ExitCode::OK; } /** @@ -365,13 +373,13 @@ abstract class BaseMigrateController extends Controller public function actionTo($version) { if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) { - $this->migrateToVersion($namespaceVersion); + return $this->migrateToVersion($namespaceVersion); } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) { - $this->migrateToVersion($migrationName); + return $this->migrateToVersion($migrationName); } elseif ((string) (int) $version == $version) { - $this->migrateToTime($version); + return $this->migrateToTime($version); } elseif (($time = strtotime($version)) !== false) { - $this->migrateToTime($time); + return $this->migrateToTime($time); } else { throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401),\n the full name of a migration (e.g. m101129_185401_create_user_table),\n the full namespaced name of a migration (e.g. app\\migrations\\M101129185401CreateUserTable),\n a UNIX timestamp (e.g. 1392853000), or a datetime string parseable\nby the strtotime() function (e.g. 2014-02-15 13:00:50)."); } @@ -429,13 +437,11 @@ abstract class BaseMigrateController extends Controller if (strpos($migration, $version) === 0) { if ($i === 0) { $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW); - } else { - if ($this->confirm("Set migration history at $originalVersion?")) { - for ($j = 0; $j < $i; ++$j) { - $this->removeMigrationHistory($migrations[$j]); - } - $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN); + } elseif ($this->confirm("Set migration history at $originalVersion?")) { + for ($j = 0; $j < $i; ++$j) { + $this->removeMigrationHistory($migrations[$j]); } + $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN); } return ExitCode::OK; @@ -446,7 +452,7 @@ abstract class BaseMigrateController extends Controller } /** - * Truncates the whole database and starts the migration from the beginning. + * Drops all tables and related constraints. Starts the migration from the beginning. * * ``` * yii migrate/fresh @@ -458,16 +464,19 @@ abstract class BaseMigrateController extends Controller { if (YII_ENV_PROD) { $this->stdout("YII_ENV is set to 'prod'.\nRefreshing migrations is not possible on production systems.\n"); + return ExitCode::OK; } - if ($this->confirm( - "Are you sure you want to reset the database and start the migration from the beginning?\nAll data will be lost irreversibly!")) { + if ($this->confirm("Are you sure you want to drop all tables and related constraints and start the migration from the beginning?\nAll data will be lost irreversibly!")) { $this->truncateDatabase(); - $this->actionUp(); - } else { - $this->stdout('Action was cancelled by user. Nothing has been performed.'); + + return $this->actionUp(); } + + $this->stdout('Action was cancelled by user. Nothing has been performed.'); + + return ExitCode::OK; } /** @@ -542,6 +551,8 @@ abstract class BaseMigrateController extends Controller $this->stdout("\t(" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"); } } + + return ExitCode::OK; } /** @@ -588,6 +599,8 @@ abstract class BaseMigrateController extends Controller $this->stdout("\t" . $migration . "\n"); } } + + return ExitCode::OK; } /** @@ -644,9 +657,16 @@ abstract class BaseMigrateController extends Controller 'namespace' => $namespace, ]); FileHelper::createDirectory($migrationPath); - file_put_contents($file, $content, LOCK_EX); + if (file_put_contents($file, $content, LOCK_EX) === false) { + $this->stdout("Failed to create new migration.\n", Console::FG_RED); + + return ExitCode::IOERR; + } + $this->stdout("New migration created successfully.\n", Console::FG_GREEN); } + + return ExitCode::OK; } /** @@ -823,8 +843,10 @@ abstract class BaseMigrateController extends Controller if ($count === 0) { $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN); } else { - $this->actionDown($count); + return $this->actionDown($count); } + + return ExitCode::OK; } /** @@ -841,9 +863,7 @@ abstract class BaseMigrateController extends Controller $migrations = $this->getNewMigrations(); foreach ($migrations as $i => $migration) { if (strpos($migration, $version) === 0) { - $this->actionUp($i + 1); - - return ExitCode::OK; + return $this->actionUp($i + 1); } } @@ -854,7 +874,7 @@ abstract class BaseMigrateController extends Controller if ($i === 0) { $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW); } else { - $this->actionDown($i); + return $this->actionDown($i); } return ExitCode::OK; diff --git a/framework/console/controllers/FixtureController.php b/framework/console/controllers/FixtureController.php index da7fc1e..cce951f 100644 --- a/framework/console/controllers/FixtureController.php +++ b/framework/console/controllers/FixtureController.php @@ -450,7 +450,7 @@ class FixtureController extends Controller $fullFixturePath = FileHelper::normalizePath($fullFixturePath); $relativeName = substr($fullFixturePath, strlen($fixturesPath) + 1); - $relativeDir = dirname($relativeName) === '.' ? '' : dirname($relativeName) . DIRECTORY_SEPARATOR; + $relativeDir = dirname($relativeName) === '.' ? '' : dirname($relativeName) . '/'; return $relativeDir . basename($fullFixturePath, 'Fixture.php'); } diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index b4db783..5185f4e 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -30,7 +30,7 @@ use yii\helpers\Inflector; * In the above, if the command name is not provided, all * available commands will be displayed. * - * @property array $commands All available command names. This property is read-only. + * @property-read array $commands All available command names. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/console/controllers/MessageController.php b/framework/console/controllers/MessageController.php index 0aa90be..a8278d9 100644 --- a/framework/console/controllers/MessageController.php +++ b/framework/console/controllers/MessageController.php @@ -386,7 +386,7 @@ EOD; if (!$removeUnused) { foreach ($obsolete as $pk => $msg) { - if (mb_substr($msg, 0, 2) === '@@' && mb_substr($msg, -2) === '@@') { + if (strpos($msg, '@@') === 0 && substr($msg, -2) === '@@') { unset($obsolete[$pk]); } } diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index 4d89ebb..aa2625e 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -581,7 +581,7 @@ class MigrateController extends BaseMigrateController protected function splitFieldIntoChunks($field) { $hasDoubleQuotes = false; - preg_match_all('/defaultValue\(.*?:.*?\)/', $field, $matches); + preg_match_all('/defaultValue\(["\'].*?:?.*?["\']\)/', $field, $matches, PREG_SET_ORDER, 0); if (isset($matches[0][0])) { $hasDoubleQuotes = true; $originalDefaultValue = $matches[0][0]; diff --git a/framework/console/widgets/Table.php b/framework/console/widgets/Table.php index ef4ec6f..99fd5d2 100644 --- a/framework/console/widgets/Table.php +++ b/framework/console/widgets/Table.php @@ -40,6 +40,9 @@ use yii\helpers\Console; * ], * ]); * + * @property-write string $listPrefix List prefix. This property is write-only. + * @property-write int $screenWidth Screen width. This property is write-only. + * * @author Daniel Gomez Pan * @since 2.0.13 */ @@ -131,7 +134,11 @@ class Table extends Widget */ public function setRows(array $rows) { - $this->rows = array_map('array_values', $rows); + $this->rows = array_map(function($row) { + return array_map(function($value) { + return empty($value) && !is_numeric($value) ? ' ' : $value; + }, array_values($row)); + }, $rows); return $this; } @@ -236,7 +243,7 @@ class Table extends Widget $buffer = ''; $arrayPointer = []; - $finalChunk = []; + $renderedChunkTexts = []; for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) { $buffer .= $spanLeft . ' '; foreach ($size as $index => $cellSize) { @@ -246,27 +253,28 @@ class Table extends Widget $buffer .= $spanMiddle . ' '; } if (is_array($cell)) { - if (empty($finalChunk[$index])) { - $finalChunk[$index] = ''; + if (empty($renderedChunkTexts[$index])) { + $renderedChunkTexts[$index] = ''; $start = 0; $prefix = $this->listPrefix; if (!isset($arrayPointer[$index])) { $arrayPointer[$index] = 0; } } else { - $start = mb_strwidth($finalChunk[$index], Yii::$app->charset); + $start = mb_strwidth($renderedChunkTexts[$index], Yii::$app->charset); } - $chunk = mb_substr($cell[$arrayPointer[$index]], $start, $cellSize - 4, Yii::$app->charset); - $finalChunk[$index] .= $chunk; - if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) { + $chunk = Console::ansiColorizedSubstr($cell[$arrayPointer[$index]], $start, $cellSize - 4); + $renderedChunkTexts[$index] .= Console::stripAnsiFormat($chunk); + $fullChunkText = Console::stripAnsiFormat($cell[$arrayPointer[$index]]); + if (isset($cell[$arrayPointer[$index] + 1]) && $renderedChunkTexts[$index] === $fullChunkText) { $arrayPointer[$index]++; - $finalChunk[$index] = ''; + $renderedChunkTexts[$index] = ''; } } else { - $chunk = mb_substr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2, Yii::$app->charset); + $chunk = Console::ansiColorizedSubstr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2); } $chunk = $prefix . $chunk; - $repeat = $cellSize - mb_strwidth($chunk, Yii::$app->charset) - 1; + $repeat = $cellSize - Console::ansiStrwidth($chunk) - 1; $buffer .= $chunk; if ($repeat >= 0) { $buffer .= str_repeat(' ', $repeat); @@ -329,25 +337,31 @@ class Table extends Widget foreach ($columns as $column) { $columnWidth = max(array_map(function ($val) { if (is_array($val)) { - $encodings = array_fill(0, count($val), Yii::$app->charset); - return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->listPrefix, Yii::$app->charset); + return max(array_map('yii\helpers\Console::ansiStrwidth', $val)) + Console::ansiStrwidth($this->listPrefix); } - - return mb_strwidth($val, Yii::$app->charset); + return Console::ansiStrwidth($val); }, $column)) + 2; $this->columnWidths[] = $columnWidth; $totalWidth += $columnWidth; } - $relativeWidth = $screenWidth / $totalWidth; - if ($totalWidth > $screenWidth) { + $minWidth = 3; + $fixWidths = []; + $relativeWidth = $screenWidth / $totalWidth; + foreach ($this->columnWidths as $j => $width) { + $scaledWidth = (int) ($width * $relativeWidth); + if ($scaledWidth < $minWidth) { + $fixWidths[$j] = 3; + } + } + + $totalFixWidth = array_sum($fixWidths); + $relativeWidth = ($screenWidth - $totalFixWidth) / ($totalWidth - $totalFixWidth); foreach ($this->columnWidths as $j => $width) { - $this->columnWidths[$j] = (int) ($width * $relativeWidth); - if ($j === count($this->columnWidths)) { - $this->columnWidths = $totalWidth; + if (!array_key_exists($j, $fixWidths)) { + $this->columnWidths[$j] = (int) ($width * $relativeWidth); } - $totalWidth -= $this->columnWidths[$j]; } } } @@ -365,23 +379,17 @@ class Table extends Widget if (is_array($columnWidth)) { $rows = 0; foreach ($columnWidth as $width) { - $rows += ceil($width / ($size - 2)); + $rows += $size == 2 ? 0 : ceil($width / ($size - 2)); } - return $rows; } - - return ceil($columnWidth / ($size - 2)); + return $size == 2 || $columnWidth == 0 ? 0 : ceil($columnWidth / ($size - 2)); }, $this->columnWidths, array_map(function ($val) { if (is_array($val)) { - $encodings = array_fill(0, count($val), Yii::$app->charset); - return array_map('mb_strwidth', $val, $encodings); + return array_map('yii\helpers\Console::ansiStrwidth', $val); } - - return mb_strwidth($val, Yii::$app->charset); - }, $row) - ); - + return Console::ansiStrwidth($val); + }, $row)); return max($rowsPerCell); } diff --git a/framework/data/ActiveDataFilter.php b/framework/data/ActiveDataFilter.php index 5ca6553..88ad362 100644 --- a/framework/data/ActiveDataFilter.php +++ b/framework/data/ActiveDataFilter.php @@ -54,7 +54,7 @@ class ActiveDataFilter extends DataFilter * * Usually the map can be left empty as filter operator names are consistent with the ones * used in [[\yii\db\QueryInterface::where()]]. However, you may want to adjust it in some special cases. - * For example, when using PosgreSQL you may want to setup the following map: + * For example, when using PostgreSQL you may want to setup the following map: * * ```php * [ diff --git a/framework/data/ActiveDataProvider.php b/framework/data/ActiveDataProvider.php index 5a4fbb8..993bb9a 100644 --- a/framework/data/ActiveDataProvider.php +++ b/framework/data/ActiveDataProvider.php @@ -56,12 +56,11 @@ use yii\di\Instance; class ActiveDataProvider extends BaseDataProvider { /** - * @var QueryInterface the query that is used to fetch data models and [[totalCount]] - * if it is not explicitly set. + * @var QueryInterface|null the query that is used to fetch data models and [[totalCount]] if it is not explicitly set. */ public $query; /** - * @var string|callable the column that is used as the key of the data models. + * @var string|callable|null the column that is used as the key of the data models. * This can be either a column name, or a callable that returns the key value of a given data model. * * If this is not set, the following rules will be used to determine the keys of the data models: @@ -73,8 +72,8 @@ class ActiveDataProvider extends BaseDataProvider */ public $key; /** - * @var Connection|array|string the DB connection object or the application component ID of the DB connection. - * If not set, the default DB connection will be used. + * @var Connection|array|string|null the DB connection object or the application component ID of the DB connection. + * If set it overrides [[query]] default DB connection. * Starting from version 2.0.2, this can also be a configuration array for creating the object. */ public $db; @@ -82,13 +81,13 @@ class ActiveDataProvider extends BaseDataProvider /** * Initializes the DB connection component. - * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * This method will initialize the [[db]] property (when set) to make sure it refers to a valid DB connection. * @throws InvalidConfigException if [[db]] is invalid. */ public function init() { parent::init(); - if (is_string($this->db)) { + if ($this->db !== null) { $this->db = Instance::ensure($this->db, Connection::className()); } } @@ -175,7 +174,7 @@ class ActiveDataProvider extends BaseDataProvider public function setSort($value) { parent::setSort($value); - if (($sort = $this->getSort()) !== false && $this->query instanceof ActiveQueryInterface) { + if ($this->query instanceof ActiveQueryInterface && ($sort = $this->getSort()) !== false) { /* @var $modelClass Model */ $modelClass = $this->query->modelClass; $model = $modelClass::instance(); diff --git a/framework/data/ArrayDataProvider.php b/framework/data/ArrayDataProvider.php index 45ee2ce..5e50780 100644 --- a/framework/data/ArrayDataProvider.php +++ b/framework/data/ArrayDataProvider.php @@ -136,7 +136,7 @@ class ArrayDataProvider extends BaseDataProvider { $orders = $sort->getOrders(); if (!empty($orders)) { - ArrayHelper::multisort($models, array_keys($orders), array_values($orders)); + ArrayHelper::multisort($models, array_keys($orders), array_values($orders), $sort->sortFlags); } return $models; diff --git a/framework/data/BaseDataProvider.php b/framework/data/BaseDataProvider.php index 82ac0d7..97214cf 100644 --- a/framework/data/BaseDataProvider.php +++ b/framework/data/BaseDataProvider.php @@ -16,15 +16,15 @@ use yii\base\InvalidArgumentException; * * For more details and usage information on BaseDataProvider, see the [guide article on data providers](guide:output-data-providers). * - * @property int $count The number of data models in the current page. This property is read-only. + * @property-read int $count The number of data models in the current page. This property is read-only. * @property array $keys The list of key values corresponding to [[models]]. Each data model in [[models]] is * uniquely identified by the corresponding key value in this array. * @property array $models The list of data models in the current page. * @property Pagination|false $pagination The pagination object. If this is false, it means the pagination is - * disabled. Note that the type of this property differs in getter and setter. See [[getPagination()]] and + * disabled. Note that the type of this property differs in getter and setter. See [[getPagination()]] and * [[setPagination()]] for details. * @property Sort|bool $sort The sorting object. If this is false, it means the sorting is disabled. Note that - * the type of this property differs in getter and setter. See [[getSort()]] and [[setSort()]] for details. + * the type of this property differs in getter and setter. See [[getSort()]] and [[setSort()]] for details. * @property int $totalCount Total number of possible data models. * * @author Qiang Xue diff --git a/framework/data/DataFilter.php b/framework/data/DataFilter.php index a10177f..18f218f 100644 --- a/framework/data/DataFilter.php +++ b/framework/data/DataFilter.php @@ -113,12 +113,13 @@ use yii\validators\Validator; * @see ActiveDataFilter * * @property array $errorMessages Error messages in format `[errorKey => message]`. Note that the type of this - * property differs in getter and setter. See [[getErrorMessages()]] and [[setErrorMessages()]] for details. + * property differs in getter and setter. See [[getErrorMessages()]] and [[setErrorMessages()]] for details. * @property mixed $filter Raw filter value. * @property array $searchAttributeTypes Search attribute type map. Note that the type of this property - * differs in getter and setter. See [[getSearchAttributeTypes()]] and [[setSearchAttributeTypes()]] for details. + * differs in getter and setter. See [[getSearchAttributeTypes()]] and [[setSearchAttributeTypes()]] for + * details. * @property Model $searchModel Model instance. Note that the type of this property differs in getter and - * setter. See [[getSearchModel()]] and [[setSearchModel()]] for details. + * setter. See [[getSearchModel()]] and [[setSearchModel()]] for details. * * @author Paul Klimov * @since 2.0.13 @@ -239,6 +240,11 @@ class DataFilter extends Model * Attribute map will be applied to filter condition in [[normalize()]] method. */ public $attributeMap = []; + /** + * @var string representation of `null` instead of literal `null` in case the latter cannot be used. + * @since 2.0.40 + */ + public $nullValue = 'NULL'; /** * @var array|\Closure list of error messages responding to invalid filter structure, in format: `[errorKey => message]`. @@ -359,24 +365,24 @@ class DataFilter extends Model if ($validator instanceof BooleanValidator) { return self::TYPE_BOOLEAN; } - + if ($validator instanceof NumberValidator) { return $validator->integerOnly ? self::TYPE_INTEGER : self::TYPE_FLOAT; } - + if ($validator instanceof StringValidator) { return self::TYPE_STRING; } - + if ($validator instanceof EachValidator) { return self::TYPE_ARRAY; } - + if ($validator instanceof DateValidator) { if ($validator->type == DateValidator::TYPE_DATETIME) { return self::TYPE_DATETIME; } - + if ($validator->type == DateValidator::TYPE_TIME) { return self::TYPE_TIME; } @@ -659,7 +665,7 @@ class DataFilter extends Model return; } - $model->{$attribute} = $value; + $model->{$attribute} = $value === $this->nullValue ? null : $value; if (!$model->validate([$attribute])) { $this->addError($this->filterAttributeName, $model->getFirstError($attribute)); return; @@ -753,6 +759,8 @@ class DataFilter extends Model } if (is_array($value)) { $result[$key] = $this->normalizeComplexFilter($value); + } elseif ($value === $this->nullValue) { + $result[$key] = null; } else { $result[$key] = $value; } diff --git a/framework/data/Pagination.php b/framework/data/Pagination.php index c8c12d0..f593d9a 100644 --- a/framework/data/Pagination.php +++ b/framework/data/Pagination.php @@ -58,15 +58,15 @@ use yii\web\Request; * * For more details and usage information on Pagination, see the [guide article on pagination](guide:output-pagination). * - * @property int $limit The limit of the data. This may be used to set the LIMIT value for a SQL statement for - * fetching the current page of data. Note that if the page size is infinite, a value -1 will be returned. This - * property is read-only. - * @property array $links The links for navigational purpose. The array keys specify the purpose of the links - * (e.g. [[LINK_FIRST]]), and the array values are the corresponding URLs. This property is read-only. - * @property int $offset The offset of the data. This may be used to set the OFFSET value for a SQL statement - * for fetching the current page of data. This property is read-only. + * @property-read int $limit The limit of the data. This may be used to set the LIMIT value for a SQL + * statement for fetching the current page of data. Note that if the page size is infinite, a value -1 will be + * returned. This property is read-only. + * @property-read array $links The links for navigational purpose. The array keys specify the purpose of the + * links (e.g. [[LINK_FIRST]]), and the array values are the corresponding URLs. This property is read-only. + * @property-read int $offset The offset of the data. This may be used to set the OFFSET value for a SQL + * statement for fetching the current page of data. This property is read-only. * @property int $page The zero-based current page number. - * @property int $pageCount Number of pages. This property is read-only. + * @property-read int $pageCount Number of pages. This property is read-only. * @property int $pageSize The number of items per page. If it is less than 1, it means the page size is * infinite, and thus a single page contains all items. * @@ -213,7 +213,7 @@ class Pagination extends BaseObject implements Linkable public function getPageSize() { if ($this->_pageSize === null) { - if (empty($this->pageSizeLimit)) { + if (empty($this->pageSizeLimit) || !isset($this->pageSizeLimit[0], $this->pageSizeLimit[1])) { $pageSize = $this->defaultPageSize; $this->setPageSize($pageSize); } else { @@ -235,7 +235,7 @@ class Pagination extends BaseObject implements Linkable $this->_pageSize = null; } else { $value = (int) $value; - if ($validatePageSize && isset($this->pageSizeLimit[0], $this->pageSizeLimit[1]) && count($this->pageSizeLimit) === 2) { + if ($validatePageSize && isset($this->pageSizeLimit[0], $this->pageSizeLimit[1])) { if ($value < $this->pageSizeLimit[0]) { $value = $this->pageSizeLimit[0]; } elseif ($value > $this->pageSizeLimit[1]) { @@ -319,16 +319,17 @@ class Pagination extends BaseObject implements Linkable { $currentPage = $this->getPage(); $pageCount = $this->getPageCount(); - $links = [ - Link::REL_SELF => $this->createUrl($currentPage, null, $absolute), - ]; - if ($currentPage > 0) { + + $links = [Link::REL_SELF => $this->createUrl($currentPage, null, $absolute)]; + if ($pageCount > 0) { $links[self::LINK_FIRST] = $this->createUrl(0, null, $absolute); - $links[self::LINK_PREV] = $this->createUrl($currentPage - 1, null, $absolute); - } - if ($currentPage < $pageCount - 1) { - $links[self::LINK_NEXT] = $this->createUrl($currentPage + 1, null, $absolute); $links[self::LINK_LAST] = $this->createUrl($pageCount - 1, null, $absolute); + if ($currentPage > 0) { + $links[self::LINK_PREV] = $this->createUrl($currentPage - 1, null, $absolute); + } + if ($currentPage < $pageCount - 1) { + $links[self::LINK_NEXT] = $this->createUrl($currentPage + 1, null, $absolute); + } } return $links; diff --git a/framework/data/Sort.php b/framework/data/Sort.php index 7ee59d2..d96178e 100644 --- a/framework/data/Sort.php +++ b/framework/data/Sort.php @@ -70,9 +70,9 @@ use yii\web\Request; * * @property array $attributeOrders Sort directions indexed by attribute names. Sort direction can be either * `SORT_ASC` for ascending order or `SORT_DESC` for descending order. Note that the type of this property - * differs in getter and setter. See [[getAttributeOrders()]] and [[setAttributeOrders()]] for details. - * @property array $orders The columns (keys) and their corresponding sort directions (values). This can be - * passed to [[\yii\db\Query::orderBy()]] to construct a DB query. This property is read-only. + * differs in getter and setter. See [[getAttributeOrders()]] and [[setAttributeOrders()]] for details. + * @property-read array $orders The columns (keys) and their corresponding sort directions (values). This can + * be passed to [[\yii\db\Query::orderBy()]] to construct a DB query. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -185,6 +185,12 @@ class Sort extends BaseObject * the `urlManager` application component will be used. */ public $urlManager; + /** + * @var int Allow to control a value of the fourth parameter which will be + * passed to [[ArrayHelper::multisort()]] + * @since 2.0.33 + */ + public $sortFlags = SORT_REGULAR; /** @@ -336,7 +342,7 @@ class Sort extends BaseObject /** * Returns the sort direction of the specified attribute in the current request. * @param string $attribute the attribute name - * @return bool|null Sort direction of the attribute. Can be either `SORT_ASC` + * @return int|null Sort direction of the attribute. Can be either `SORT_ASC` * for ascending order or `SORT_DESC` for descending order. Null is returned * if the attribute is invalid or does not need to be sorted. */ diff --git a/framework/db/ActiveQuery.php b/framework/db/ActiveQuery.php index b69b7ef..300f9dd 100644 --- a/framework/db/ActiveQuery.php +++ b/framework/db/ActiveQuery.php @@ -486,6 +486,16 @@ class ActiveQuery extends Query implements ActiveQueryInterface } $this->join = array_values($uniqueJoins); + // https://github.com/yiisoft/yii2/issues/16092 + $uniqueJoinsByTableName = []; + foreach ($this->join as $config) { + $tableName = serialize($config[1]); + if (!array_key_exists($tableName, $uniqueJoinsByTableName)) { + $uniqueJoinsByTableName[$tableName] = $config; + } + } + $this->join = array_values($uniqueJoinsByTableName); + if (!empty($join)) { // append explicit join to joinWith() // https://github.com/yiisoft/yii2/issues/2880 diff --git a/framework/db/ActiveQueryInterface.php b/framework/db/ActiveQueryInterface.php index 9150e8b..8c70807 100644 --- a/framework/db/ActiveQueryInterface.php +++ b/framework/db/ActiveQueryInterface.php @@ -53,6 +53,8 @@ interface ActiveQueryInterface extends QueryInterface * // return the index value corresponding to $model * } * ``` + * The column has to be a part of the `SELECT` fragment of a SQL statement. + * If [[yii\db\Query::select()|select()]] is used with an array in its parameter, Yii handles adding that required SQL fragment for you. * * @return $this the query object itself */ diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 9af953c..0ccbcca 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -173,7 +173,7 @@ class ActiveRecord extends BaseActiveRecord { $query = static::find(); - if (!ArrayHelper::isAssociative($condition)) { + if (!ArrayHelper::isAssociative($condition) && !$condition instanceof ExpressionInterface) { // query by primary key $primaryKey = static::primaryKey(); if (isset($primaryKey[0])) { @@ -289,7 +289,7 @@ class ActiveRecord extends BaseActiveRecord $query->where($pk); /* @var $record BaseActiveRecord */ - $record = $query->one(); + $record = $query->noCache()->one(); return $this->refreshInternal($record); } diff --git a/framework/db/ActiveRecordInterface.php b/framework/db/ActiveRecordInterface.php index 65fc189..dfa9ed3 100644 --- a/framework/db/ActiveRecordInterface.php +++ b/framework/db/ActiveRecordInterface.php @@ -164,6 +164,7 @@ interface ActiveRecordInterface extends StaticInstanceInterface * - an associative array of name-value pairs: query by a set of attribute values and return a single record * matching all of them (or `null` if not found). Note that `['id' => 1, 2]` is treated as a non-associative array. * Column names are limited to current records table columns for SQL DBMS, or filtered otherwise to be limited to simple filter conditions. + * - a yii\db\Expression: The expression will be used directly. (@since 2.0.37) * * That this method will automatically call the `one()` method and return an [[ActiveRecordInterface|ActiveRecord]] * instance. @@ -231,6 +232,7 @@ interface ActiveRecordInterface extends StaticInstanceInterface * matching all of them (or an empty array if none was found). Note that `['id' => 1, 2]` is treated as * a non-associative array. * Column names are limited to current records table columns for SQL DBMS, or filtered otherwise to be limted to simple filter conditions. + * - a yii\db\Expression: The expression will be used directly. (@since 2.0.37) * * This method will automatically call the `all()` method and return an array of [[ActiveRecordInterface|ActiveRecord]] * instances. @@ -294,7 +296,7 @@ interface ActiveRecordInterface extends StaticInstanceInterface * * @param array $attributes attribute values (name-value pairs) to be saved for the record. * Unlike [[update()]] these are not going to be validated. - * @param array $condition the condition that matches the records that should get updated. + * @param mixed $condition the condition that matches the records that should get updated. * Please refer to [[QueryInterface::where()]] on how to specify this parameter. * An empty condition will match all records. * @return int the number of rows updated diff --git a/framework/db/ActiveRelationTrait.php b/framework/db/ActiveRelationTrait.php index e463f3c..6f6e12c 100644 --- a/framework/db/ActiveRelationTrait.php +++ b/framework/db/ActiveRelationTrait.php @@ -17,8 +17,8 @@ use yii\base\InvalidConfigException; * @author Carsten Brandt * @since 2.0 * - * @method ActiveRecordInterface one() - * @method ActiveRecordInterface[] all() + * @method ActiveRecordInterface one($db = null) + * @method ActiveRecordInterface[] all($db = null) * @property ActiveRecord $modelClass */ trait ActiveRelationTrait @@ -242,7 +242,7 @@ trait ActiveRelationTrait $viaQuery->asArray($this->asArray); } $viaQuery->primaryModel = null; - $viaModels = $viaQuery->populateRelation($viaName, $primaryModels); + $viaModels = array_filter($viaQuery->populateRelation($viaName, $primaryModels)); $this->filterByModels($viaModels); } else { $this->filterByModels($primaryModels); @@ -585,22 +585,23 @@ trait ActiveRelationTrait if (count($key) > 1) { return serialize($key); } - $key = reset($key); - return is_scalar($key) ? $key : serialize($key); + return reset($key); } /** - * @param mixed $value raw key value. + * @param mixed $value raw key value. Since 2.0.40 non-string values must be convertable to string (like special + * objects for cross-DBMS relations, for example: `|MongoId`). * @return string normalized key value. */ private function normalizeModelKey($value) { - if (is_object($value) && method_exists($value, '__toString')) { - // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId` - $value = $value->__toString(); + try { + return (string)$value; + } catch (\Exception $e) { + throw new InvalidConfigException('Value must be convertable to string.'); + } catch (\Throwable $e) { + throw new InvalidConfigException('Value must be convertable to string.'); } - - return $value; } /** diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index cc47375..ead5f5c 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -23,19 +23,19 @@ use yii\helpers\ArrayHelper; * * See [[\yii\db\ActiveRecord]] for a concrete implementation. * - * @property array $dirtyAttributes The changed attribute values (name-value pairs). This property is + * @property-read array $dirtyAttributes The changed attribute values (name-value pairs). This property is * read-only. * @property bool $isNewRecord Whether the record is new and should be inserted when calling [[save()]]. * @property array $oldAttributes The old attribute values (name-value pairs). Note that the type of this - * property differs in getter and setter. See [[getOldAttributes()]] and [[setOldAttributes()]] for details. - * @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is + * property differs in getter and setter. See [[getOldAttributes()]] and [[setOldAttributes()]] for details. + * @property-read mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is * returned if the primary key is composite. A string is returned otherwise (null will be returned if the key * value is null). This property is read-only. - * @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if - * the primary key is composite. A string is returned otherwise (null will be returned if the key value is null). - * This property is read-only. - * @property array $relatedRecords An array of related records indexed by relation names. This property is - * read-only. + * @property-read mixed $primaryKey The primary key value. An array (column name => column value) is returned + * if the primary key is composite. A string is returned otherwise (null will be returned if the key value is + * null). This property is read-only. + * @property-read array $relatedRecords An array of related records indexed by relation names. This property + * is read-only. * * @author Qiang Xue * @author Carsten Brandt @@ -133,7 +133,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface { $query = static::find(); - if (!ArrayHelper::isAssociative($condition)) { + if (!ArrayHelper::isAssociative($condition) && !$condition instanceof ExpressionInterface) { // query by primary key $primaryKey = static::primaryKey(); if (isset($primaryKey[0])) { diff --git a/framework/db/BatchQueryResult.php b/framework/db/BatchQueryResult.php index 68fd03f..5158e4b 100644 --- a/framework/db/BatchQueryResult.php +++ b/framework/db/BatchQueryResult.php @@ -7,7 +7,7 @@ namespace yii\db; -use yii\base\BaseObject; +use yii\base\Component; /** * BatchQueryResult represents a batch query from which you can retrieve data in batches. @@ -28,9 +28,21 @@ use yii\base\BaseObject; * @author Qiang Xue * @since 2.0 */ -class BatchQueryResult extends BaseObject implements \Iterator +class BatchQueryResult extends Component implements \Iterator { /** + * @event Event an event that is triggered when the batch query is reset. + * @see reset() + * @since 2.0.41 + */ + const EVENT_RESET = 'reset'; + /** + * @event Event an event that is triggered when the last batch has been fetched. + * @since 2.0.41 + */ + const EVENT_FINISH = 'finish'; + + /** * @var Connection the DB connection to be used when performing batch query. * If null, the "db" application component will be used. */ @@ -95,6 +107,7 @@ class BatchQueryResult extends BaseObject implements \Iterator $this->_batch = null; $this->_value = null; $this->_key = null; + $this->trigger(self::EVENT_RESET); } /** @@ -160,8 +173,14 @@ class BatchQueryResult extends BaseObject implements \Iterator $count = 0; try { - while ($count++ < $this->batchSize && ($row = $this->_dataReader->read())) { - $rows[] = $row; + while ($count++ < $this->batchSize) { + if ($row = $this->_dataReader->read()) { + $rows[] = $row; + } else { + // we've reached the end + $this->trigger(self::EVENT_FINISH); + break; + } } } catch (\PDOException $e) { $errorCode = isset($e->errorInfo[1]) ? $e->errorInfo[1] : null; @@ -223,4 +242,15 @@ class BatchQueryResult extends BaseObject implements \Iterator return null; } + + /** + * Unserialization is disabled to prevent remote code execution in case application + * calls unserialize() on user input containing specially crafted string. + * @see CVE-2020-15148 + * @since 2.0.38 + */ + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize ' . __CLASS__); + } } diff --git a/framework/db/Command.php b/framework/db/Command.php index 6735607..1dcfb5b 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -90,8 +90,10 @@ class Command extends Component /** * @var array pending parameters to be bound to the current PDO statement. + * @since 2.0.33 */ - private $_pendingParams = []; + protected $pendingParams = []; + /** * @var string the SQL statement that this command represents */ @@ -205,13 +207,13 @@ class Command extends Component if (is_string($name) && strncmp(':', $name, 1)) { $name = ':' . $name; } - if (is_string($value)) { - $params[$name] = $this->db->quoteValue($value); + if (is_string($value) || $value instanceof Expression) { + $params[$name] = $this->db->quoteValue((string)$value); } elseif (is_bool($value)) { $params[$name] = ($value ? 'TRUE' : 'FALSE'); } elseif ($value === null) { $params[$name] = 'NULL'; - } elseif ((!is_object($value) && !is_resource($value)) || $value instanceof Expression) { + } elseif (!is_object($value) && !is_resource($value)) { $params[$name] = $value; } } @@ -244,6 +246,9 @@ class Command extends Component } $sql = $this->getSql(); + if ($sql === '') { + return; + } if ($this->db->getTransaction()) { // master is in a transaction. use the same connection. @@ -262,6 +267,9 @@ class Command extends Component $message = $e->getMessage() . "\nFailed to prepare SQL: $sql"; $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int) $e->getCode(), $e); + } catch (\Throwable $e) { + $message = $e->getMessage() . "\nFailed to prepare SQL: $sql"; + throw new Exception($message, null, (int) $e->getCode(), $e); } } @@ -312,10 +320,10 @@ class Command extends Component */ protected function bindPendingParams() { - foreach ($this->_pendingParams as $name => $value) { + foreach ($this->pendingParams as $name => $value) { $this->pdoStatement->bindValue($name, $value[0], $value[1]); } - $this->_pendingParams = []; + $this->pendingParams = []; } /** @@ -334,7 +342,7 @@ class Command extends Component if ($dataType === null) { $dataType = $this->db->getSchema()->getPdoType($value); } - $this->_pendingParams[$name] = [$value, $dataType]; + $this->pendingParams[$name] = [$value, $dataType]; $this->params[$name] = $value; return $this; @@ -360,14 +368,14 @@ class Command extends Component $schema = $this->db->getSchema(); foreach ($values as $name => $value) { if (is_array($value)) { // TODO: Drop in Yii 2.1 - $this->_pendingParams[$name] = $value; + $this->pendingParams[$name] = $value; $this->params[$name] = $value[0]; } elseif ($value instanceof PdoValue) { - $this->_pendingParams[$name] = [$value->getValue(), $value->getType()]; + $this->pendingParams[$name] = [$value->getValue(), $value->getType()]; $this->params[$name] = $value->getValue(); } else { $type = $schema->getPdoType($value); - $this->_pendingParams[$name] = [$value, $type]; + $this->pendingParams[$name] = [$value, $type]; $this->params[$name] = $value; } } @@ -416,7 +424,7 @@ class Command extends Component /** * Executes the SQL statement and returns the value of the first column in the first row of data. * This method is best used when only a single value is needed for a query. - * @return string|null|false the value of the first column in the first row of the query result. + * @return string|int|null|false the value of the first column in the first row of the query result. * False is returned if there is no value. * @throws Exception execution failed */ @@ -1140,8 +1148,7 @@ class Command extends Component if (is_array($info)) { /* @var $cache \yii\caching\CacheInterface */ $cache = $info[0]; - $rawSql = $rawSql ?: $this->getRawSql(); - $cacheKey = $this->getCacheKey($method, $fetchMode, $rawSql); + $cacheKey = $this->getCacheKey($method, $fetchMode, ''); $result = $cache->get($cacheKey); if (is_array($result) && isset($result[0])) { Yii::debug('Query result served from cache', 'yii\db\Command::query'); @@ -1187,19 +1194,21 @@ class Command extends Component * @param string $method method of PDOStatement to be called * @param int $fetchMode the result fetch mode. Please refer to [PHP manual](https://secure.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. - * @param string $rawSql the raw SQL with parameter values inserted into the corresponding placeholders * @return array the cache key * @since 2.0.16 */ protected function getCacheKey($method, $fetchMode, $rawSql) { + $params = $this->params; + ksort($params); return [ __CLASS__, $method, $fetchMode, $this->db->dsn, $this->db->username, - $rawSql, + $this->getSql(), + json_encode($params), ]; } @@ -1308,7 +1317,7 @@ class Command extends Component protected function reset() { $this->_sql = null; - $this->_pendingParams = []; + $this->pendingParams = []; $this->params = []; $this->_refreshTableName = null; $this->_isolationLevel = false; diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 5610327..de462ba 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -111,24 +111,24 @@ use yii\caching\CacheInterface; * ``` * * @property string $driverName Name of the DB driver. - * @property bool $isActive Whether the DB connection is established. This property is read-only. - * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the - * sequence object. This property is read-only. - * @property Connection $master The currently active master connection. `null` is returned if there is no + * @property-read bool $isActive Whether the DB connection is established. This property is read-only. + * @property-read string $lastInsertID The row ID of the last row inserted, or the last value retrieved from + * the sequence object. This property is read-only. + * @property-read Connection $master The currently active master connection. `null` is returned if there is no * master available. This property is read-only. - * @property PDO $masterPdo The PDO instance for the currently active master connection. This property is + * @property-read PDO $masterPdo The PDO instance for the currently active master connection. This property is * read-only. * @property QueryBuilder $queryBuilder The query builder for the current DB connection. Note that the type of - * this property differs in getter and setter. See [[getQueryBuilder()]] and [[setQueryBuilder()]] for details. - * @property Schema $schema The schema information for the database opened by this connection. This property - * is read-only. - * @property string $serverVersion Server version as a string. This property is read-only. - * @property Connection $slave The currently active slave connection. `null` is returned if there is no slave - * available and `$fallbackToMaster` is false. This property is read-only. - * @property PDO $slavePdo The PDO instance for the currently active slave connection. `null` is returned if - * no slave connection is available and `$fallbackToMaster` is false. This property is read-only. - * @property Transaction|null $transaction The currently active transaction. Null if no active transaction. - * This property is read-only. + * this property differs in getter and setter. See [[getQueryBuilder()]] and [[setQueryBuilder()]] for details. + * @property-read Schema $schema The schema information for the database opened by this connection. This + * property is read-only. + * @property-read string $serverVersion Server version as a string. This property is read-only. + * @property-read Connection $slave The currently active slave connection. `null` is returned if there is no + * slave available and `$fallbackToMaster` is false. This property is read-only. + * @property-read PDO $slavePdo The PDO instance for the currently active slave connection. `null` is returned + * if no slave connection is available and `$fallbackToMaster` is false. This property is read-only. + * @property-read Transaction|null $transaction The currently active transaction. Null if no active + * transaction. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -317,7 +317,7 @@ class Connection extends Component 'sqlite' => 'yii\db\sqlite\Command', // sqlite 3 'sqlite2' => 'yii\db\sqlite\Command', // sqlite 2 'sqlsrv' => 'yii\db\Command', // newer MSSQL driver on MS Windows hosts - 'oci' => 'yii\db\Command', // Oracle driver + 'oci' => 'yii\db\oci\Command', // Oracle driver 'mssql' => 'yii\db\Command', // older MSSQL driver on MS Windows hosts 'dblib' => 'yii\db\Command', // dblib drivers on GNU/Linux (and maybe other OSes) hosts 'cubrid' => 'yii\db\Command', // CUBRID @@ -332,6 +332,8 @@ class Connection extends Component * the health status of the DB servers specified in [[masters]] and [[slaves]]. * This is used only when read/write splitting is enabled or [[masters]] is not empty. * Set boolean `false` to disabled server status caching. + * @see openFromPoolSequentially() for details about the failover behavior. + * @see serverRetryInterval */ public $serverStatusCache = 'cache'; /** @@ -416,6 +418,11 @@ class Connection extends Component * @see enableLogging */ public $enableProfiling = true; + /** + * @var bool If the database connected via pdo_dblib is SyBase. + * @since 2.0.38 + */ + public $isSybase = false; /** * @var Transaction the currently active transaction @@ -613,7 +620,10 @@ class Connection extends Component $token = 'Opening DB connection: ' . $this->dsn; $enableProfiling = $this->enableProfiling; try { - Yii::info($token, __METHOD__); + if ($this->enableLogging) { + Yii::info($token, __METHOD__); + } + if ($enableProfiling) { Yii::beginProfile($token, __METHOD__); } @@ -651,14 +661,19 @@ class Connection extends Component if ($this->pdo !== null) { Yii::debug('Closing DB connection: ' . $this->dsn, __METHOD__); $this->pdo = null; - $this->_schema = null; - $this->_transaction = null; } if ($this->_slave) { $this->_slave->close(); $this->_slave = false; } + + $this->_schema = null; + $this->_transaction = null; + $this->_driverName = null; + $this->_queryCacheInfo = []; + $this->_quotedTableNames = null; + $this->_quotedColumnNames = null; } /** @@ -679,8 +694,10 @@ class Connection extends Component $driver = strtolower(substr($this->dsn, 0, $pos)); } if (isset($driver)) { - if ($driver === 'mssql' || $driver === 'dblib') { + if ($driver === 'mssql') { $pdoClass = 'yii\db\mssql\PDO'; + } elseif ($driver === 'dblib') { + $pdoClass = 'yii\db\mssql\DBLibPDO'; } elseif ($driver === 'sqlsrv') { $pdoClass = 'yii\db\mssql\SqlsrvPDO'; } @@ -710,6 +727,9 @@ class Connection extends Component $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare); } } + if (!$this->isSybase && in_array($this->getDriverName(), ['mssql', 'dblib'], true)) { + $this->pdo->exec('SET ANSI_NULL_DFLT_ON ON'); + } if ($this->charset !== null && in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid'], true)) { $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset)); } @@ -1091,12 +1111,16 @@ class Connection extends Component /** * Opens the connection to a server in the pool. - * This method implements the load balancing among the given list of the servers. + * + * This method implements load balancing and failover among the given list of the servers. * Connections will be tried in random order. + * For details about the failover behavior, see [[openFromPoolSequentially]]. + * * @param array $pool the list of connection configurations in the server pool * @param array $sharedConfig the configuration common to those given in `$pool`. * @return Connection the opened DB connection, or `null` if no server is available * @throws InvalidConfigException if a configuration does not specify "dsn" + * @see openFromPoolSequentially */ protected function openFromPool(array $pool, array $sharedConfig) { @@ -1106,13 +1130,26 @@ class Connection extends Component /** * Opens the connection to a server in the pool. - * This method implements the load balancing among the given list of the servers. - * Connections will be tried in sequential order. + * + * This method implements failover among the given list of servers. + * Connections will be tried in sequential order. The first successful connection will return. + * + * If [[serverStatusCache]] is configured, this method will cache information about + * unreachable servers and does not try to connect to these for the time configured in [[serverRetryInterval]]. + * This helps to keep the application stable when some servers are unavailable. Avoiding + * connection attempts to unavailable servers saves time when the connection attempts fail due to timeout. + * + * If none of the servers are available the status cache is ignored and connection attempts are made to all + * servers (Since version 2.0.35). This is to avoid downtime when all servers are unavailable for a short time. + * After a successful connection attempt the server is marked as available again. + * * @param array $pool the list of connection configurations in the server pool * @param array $sharedConfig the configuration common to those given in `$pool`. * @return Connection the opened DB connection, or `null` if no server is available * @throws InvalidConfigException if a configuration does not specify "dsn" * @since 2.0.11 + * @see openFromPool + * @see serverStatusCache */ protected function openFromPoolSequentially(array $pool, array $sharedConfig) { @@ -1126,8 +1163,8 @@ class Connection extends Component $cache = is_string($this->serverStatusCache) ? Yii::$app->get($this->serverStatusCache, false) : $this->serverStatusCache; - foreach ($pool as $config) { - $config = array_merge($sharedConfig, $config); + foreach ($pool as $i => $config) { + $pool[$i] = $config = array_merge($sharedConfig, $config); if (empty($config['dsn'])) { throw new InvalidConfigException('The "dsn" option must be specified.'); } @@ -1150,6 +1187,30 @@ class Connection extends Component // mark this server as dead and only retry it after the specified interval $cache->set($key, 1, $this->serverRetryInterval); } + // exclude server from retry below + unset($pool[$i]); + } + } + + if ($cache instanceof CacheInterface) { + // if server status cache is enabled and no server is available + // ignore the cache and try to connect anyway + // $pool now only contains servers we did not already try in the loop above + foreach ($pool as $config) { + + /* @var $db Connection */ + $db = Yii::createObject($config); + try { + $db->open(); + } catch (\Exception $e) { + Yii::warning("Connection ({$config['dsn']}) failed: " . $e->getMessage(), __METHOD__); + continue; + } + + // mark this server as available again after successful connection + $cache->delete([__METHOD__, $config['dsn']]); + + return $db; } } diff --git a/framework/db/DataReader.php b/framework/db/DataReader.php index f2ef1eb..50a2d7d 100644 --- a/framework/db/DataReader.php +++ b/framework/db/DataReader.php @@ -40,10 +40,10 @@ use yii\base\InvalidCallException; * [[fetchMode]]. See the [PHP manual](https://secure.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for more details about possible fetch mode. * - * @property int $columnCount The number of columns in the result set. This property is read-only. - * @property int $fetchMode Fetch mode. This property is write-only. - * @property bool $isClosed Whether the reader is closed or not. This property is read-only. - * @property int $rowCount Number of rows contained in the result. This property is read-only. + * @property-read int $columnCount The number of columns in the result set. This property is read-only. + * @property-write int $fetchMode Fetch mode. This property is write-only. + * @property-read bool $isClosed Whether the reader is closed or not. This property is read-only. + * @property-read int $rowCount Number of rows contained in the result. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/db/Exception.php b/framework/db/Exception.php index fadf99c..ae16407 100644 --- a/framework/db/Exception.php +++ b/framework/db/Exception.php @@ -26,13 +26,14 @@ class Exception extends \yii\base\Exception * Constructor. * @param string $message PDO error message * @param array $errorInfo PDO error info - * @param int $code PDO error code - * @param \Exception $previous The previous exception used for the exception chaining. + * @param string $code PDO error code + * @param \Throwable|\Exception $previous The previous exception used for the exception chaining. */ - public function __construct($message, $errorInfo = [], $code = 0, \Exception $previous = null) + public function __construct($message, $errorInfo = [], $code = '', $previous = null) { + parent::__construct($message, 0, $previous); $this->errorInfo = $errorInfo; - parent::__construct($message, $code, $previous); + $this->code = $code; } /** diff --git a/framework/db/Query.php b/framework/db/Query.php index ba4056a..e8c6438 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -42,7 +42,7 @@ use yii\base\InvalidConfigException; * * A more detailed usage guide on how to work with Query can be found in the [guide article on Query Builder](guide:db-query-builder). * - * @property string[] $tablesUsedInFrom Table names indexed by aliases. This property is read-only. + * @property-read string[] $tablesUsedInFrom Table names indexed by aliases. This property is read-only. * * @author Qiang Xue * @author Carsten Brandt @@ -111,6 +111,17 @@ class Query extends Component implements QueryInterface, ExpressionInterface */ public $union; /** + * @var array this is used to construct the WITH section in a SQL query. + * Each array element is an array of the following structure: + * + * - `query`: either a string or a [[Query]] object representing a query + * - `alias`: string, alias of query for further usage + * - `recursive`: boolean, whether it should be `WITH RECURSIVE` or `WITH` + * @see withQuery() + * @since 2.0.35 + */ + public $withQueries; + /** * @var array list of query parameter values indexed by parameter placeholders. * For example, `[':name' => 'Dan', ':age' => 31]`. */ @@ -234,7 +245,29 @@ class Query extends Component implements QueryInterface, ExpressionInterface if ($this->emulateExecution) { return []; } + + if (is_string($this->indexBy) && $this->indexBy && is_array($this->select)) { + $isIndexByAnArray = false; + if (strpos($this->indexBy, '.')) { + $indexByParts = explode('.', $this->indexBy); + foreach ($indexByParts as $indexByPart) { + if (is_numeric($indexByPart)) { + $isIndexByAnArray = true; + break; + } + } + } + if (!$isIndexByAnArray && !in_array($this->indexBy, $this->select, true)) { + if (strpos($this->indexBy, '.') === false && count($tables = $this->getTablesUsedInFrom()) > 0) { + $this->select[] = key($tables) . '.' . $this->indexBy; + } else { + $this->select[] = $this->indexBy; + } + } + } + $rows = $this->createCommand($db)->queryAll(); + return $this->populate($rows); } @@ -279,7 +312,7 @@ class Query extends Component implements QueryInterface, ExpressionInterface * The value returned will be the first column in the first row of the query results. * @param Connection $db the database connection used to generate the SQL statement. * If this parameter is not given, the `db` application component will be used. - * @return string|null|false the value of the first column in the first row of the query result. + * @return string|int|null|false the value of the first column in the first row of the query result. * False is returned if the query result is empty. */ public function scalar($db = null) @@ -316,13 +349,21 @@ class Query extends Component implements QueryInterface, ExpressionInterface } $rows = $this->createCommand($db)->queryAll(); $results = []; + $column = null; + if (is_string($this->indexBy)) { + if (($dotPos = strpos($this->indexBy, '.')) === false) { + $column = $this->indexBy; + } else { + $column = substr($this->indexBy, $dotPos + 1); + } + } foreach ($rows as $row) { $value = reset($row); if ($this->indexBy instanceof \Closure) { $results[call_user_func($this->indexBy, $row)] = $value; } else { - $results[$row[$this->indexBy]] = $value; + $results[$row[$column]] = $value; } } @@ -453,13 +494,25 @@ class Query extends Component implements QueryInterface, ExpressionInterface $this->orderBy = null; $this->limit = null; $this->offset = null; - $command = $this->createCommand($db); + + $e = null; + try { + $command = $this->createCommand($db); + } catch (\Exception $e) { + // throw it later + } catch (\Throwable $e) { + // throw it later + } $this->select = $select; $this->orderBy = $order; $this->limit = $limit; $this->offset = $offset; + if ($e !== null) { + throw $e; + } + return $command->queryScalar(); } @@ -901,16 +954,18 @@ PATTERN; * Appends a JOIN part to the query. * The first parameter specifies what type of join it is. * @param string $type the type of join, such as INNER JOIN, LEFT JOIN. - * @param string|array $table the table to be joined. + * @param string|array $table the table or sub-query to be joined. * * Use a string to represent the name of the table to be joined. * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u'). * The method will automatically quote the table name unless it contains some parenthesis * (which means the table is given as a sub-query or DB expression). * - * Use an array to represent joining with a sub-query. The array must contain only one element. - * The value must be a [[Query]] object representing the sub-query while the corresponding key - * represents the alias for the sub-query. + * You may also specify the table as an array with one element, using the array key as the table alias + * (e.g. ['u' => 'user']). + * + * To join a sub-query, use an array with one element, with the value set to a [[Query]] object + * representing the sub-query, and the corresponding key representing the alias. * * @param string|array $on the join condition that should appear in the ON part. * Please refer to [[where()]] on how to specify this parameter. @@ -935,16 +990,18 @@ PATTERN; /** * Appends an INNER JOIN part to the query. - * @param string|array $table the table to be joined. + * @param string|array $table the table or sub-query to be joined. * * Use a string to represent the name of the table to be joined. * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u'). * The method will automatically quote the table name unless it contains some parenthesis * (which means the table is given as a sub-query or DB expression). * - * Use an array to represent joining with a sub-query. The array must contain only one element. - * The value must be a [[Query]] object representing the sub-query while the corresponding key - * represents the alias for the sub-query. + * You may also specify the table as an array with one element, using the array key as the table alias + * (e.g. ['u' => 'user']). + * + * To join a sub-query, use an array with one element, with the value set to a [[Query]] object + * representing the sub-query, and the corresponding key representing the alias. * * @param string|array $on the join condition that should appear in the ON part. * Please refer to [[join()]] on how to specify this parameter. @@ -959,16 +1016,18 @@ PATTERN; /** * Appends a LEFT OUTER JOIN part to the query. - * @param string|array $table the table to be joined. + * @param string|array $table the table or sub-query to be joined. * * Use a string to represent the name of the table to be joined. * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u'). * The method will automatically quote the table name unless it contains some parenthesis * (which means the table is given as a sub-query or DB expression). * - * Use an array to represent joining with a sub-query. The array must contain only one element. - * The value must be a [[Query]] object representing the sub-query while the corresponding key - * represents the alias for the sub-query. + * You may also specify the table as an array with one element, using the array key as the table alias + * (e.g. ['u' => 'user']). + * + * To join a sub-query, use an array with one element, with the value set to a [[Query]] object + * representing the sub-query, and the corresponding key representing the alias. * * @param string|array $on the join condition that should appear in the ON part. * Please refer to [[join()]] on how to specify this parameter. @@ -983,16 +1042,18 @@ PATTERN; /** * Appends a RIGHT OUTER JOIN part to the query. - * @param string|array $table the table to be joined. + * @param string|array $table the table or sub-query to be joined. * * Use a string to represent the name of the table to be joined. * The table name can contain a schema prefix (e.g. 'public.user') and/or table alias (e.g. 'user u'). * The method will automatically quote the table name unless it contains some parenthesis * (which means the table is given as a sub-query or DB expression). * - * Use an array to represent joining with a sub-query. The array must contain only one element. - * The value must be a [[Query]] object representing the sub-query while the corresponding key - * represents the alias for the sub-query. + * You may also specify the table as an array with one element, using the array key as the table alias + * (e.g. ['u' => 'user']). + * + * To join a sub-query, use an array with one element, with the value set to a [[Query]] object + * representing the sub-query, and the corresponding key representing the alias. * * @param string|array $on the join condition that should appear in the ON part. * Please refer to [[join()]] on how to specify this parameter. @@ -1223,6 +1284,20 @@ PATTERN; } /** + * Prepends a SQL statement using WITH syntax. + * @param string|Query $query the SQL statement to be prepended using WITH + * @param string $alias query alias in WITH construction + * @param bool $recursive TRUE if using WITH RECURSIVE and FALSE if using WITH + * @return $this the query object itself + * @since 2.0.35 + */ + public function withQuery($query, $alias, $recursive = false) + { + $this->withQueries[] = ['query' => $query, 'alias' => $alias, 'recursive' => $recursive]; + return $this; + } + + /** * Sets the parameters to be bound to the query. * @param array $params list of query parameter values indexed by parameter placeholders. * For example, `[':name' => 'Dan', ':age' => 31]`. @@ -1330,6 +1405,7 @@ PATTERN; 'having' => $from->having, 'union' => $from->union, 'params' => $from->params, + 'withQueries' => $from->withQueries, ]); } diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 41c7e95..c6f7487 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -22,10 +22,10 @@ use yii\helpers\StringHelper; * * For more details and usage information on QueryBuilder, see the [guide article on query builders](guide:db-query-builder). * - * @property string[] $conditionClasses Map of condition aliases to condition classes. For example: ```php - * ['LIKE' => yii\db\condition\LikeCondition::class] ``` . This property is write-only. - * @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones in - * [[expressionBuilders]] property. This property is write-only. + * @property-write string[] $conditionClasses Map of condition aliases to condition classes. For example: + * ```php ['LIKE' => yii\db\condition\LikeCondition::class] ``` . This property is write-only. + * @property-write string[] $expressionBuilders Array of builders that should be merged with the pre-defined + * ones in [[expressionBuilders]] property. This property is write-only. * * @author Qiang Xue * @since 2.0 @@ -260,6 +260,11 @@ class QueryBuilder extends \yii\base\BaseObject $sql = "($sql){$this->separator}$union"; } + $with = $this->buildWithQueries($query->withQueries, $params); + if ($with !== '') { + $sql = "$with{$this->separator}$sql"; + } + return [$sql, $params]; } @@ -1493,6 +1498,38 @@ class QueryBuilder extends \yii\base\BaseObject } /** + * @param array $withs of configurations for each WITH query + * @param array $params the binding parameters to be populated + * @return string compiled WITH prefix of query including nested queries + * @see Query::withQuery() + * @since 2.0.35 + */ + public function buildWithQueries($withs, &$params) + { + if (empty($withs)) { + return ''; + } + + $recursive = false; + $result = []; + + foreach ($withs as $i => $with) { + if ($with['recursive']) { + $recursive = true; + } + + $query = $with['query']; + if ($query instanceof Query) { + list($with['query'], $params) = $this->build($query, $params); + } + + $result[] = $with['alias'] . ' AS (' . $with['query'] . ')'; + } + + return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . implode (', ', $result); + } + + /** * Processes columns and properly quotes them if necessary. * It will join all columns into a string with comma as separators. * @param string|array $columns the columns to be processed diff --git a/framework/db/QueryInterface.php b/framework/db/QueryInterface.php index df57ad3..30df4e5 100644 --- a/framework/db/QueryInterface.php +++ b/framework/db/QueryInterface.php @@ -68,6 +68,8 @@ interface QueryInterface * // return the index value corresponding to $row * } * ``` + * The column has to be a part of the `SELECT` fragment of a SQL statement. + * If [[yii\db\Query::select()|select()]] is used with an array in its parameter, Yii handles adding that required SQL fragment for you. * * @return $this the query object itself */ diff --git a/framework/db/QueryTrait.php b/framework/db/QueryTrait.php index 803d6ba..277fd05 100644 --- a/framework/db/QueryTrait.php +++ b/framework/db/QueryTrait.php @@ -21,23 +21,23 @@ use yii\base\NotSupportedException; trait QueryTrait { /** - * @var string|array|ExpressionInterface query condition. This refers to the WHERE clause in a SQL statement. + * @var string|array|ExpressionInterface|null query condition. This refers to the WHERE clause in a SQL statement. * For example, `['age' => 31, 'team' => 1]`. * @see where() for valid syntax on specifying this value. */ public $where; /** - * @var int|ExpressionInterface maximum number of records to be returned. May be an instance of [[ExpressionInterface]]. + * @var int|ExpressionInterface|null maximum number of records to be returned. May be an instance of [[ExpressionInterface]]. * If not set or less than 0, it means no limit. */ public $limit; /** - * @var int|ExpressionInterface zero-based offset from where the records are to be returned. + * @var int|ExpressionInterface|null zero-based offset from where the records are to be returned. * May be an instance of [[ExpressionInterface]]. If not set or less than 0, it means starting from the beginning. */ public $offset; /** - * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. + * @var array|null how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which * can be either [SORT_ASC](https://secure.php.net/manual/en/array.constants.php#constant.sort-asc) * or [SORT_DESC](https://secure.php.net/manual/en/array.constants.php#constant.sort-desc). @@ -46,7 +46,7 @@ trait QueryTrait */ public $orderBy; /** - * @var string|callable the name of the column by which the query results should be indexed by. + * @var string|callable|null the name of the column by which the query results should be indexed by. * This can also be a callable (e.g. anonymous function) that returns the index value based on the given * row data. For more details, see [[indexBy()]]. This property is only used by [[QueryInterface::all()|all()]]. */ @@ -71,6 +71,8 @@ trait QueryTrait * // return the index value corresponding to $row * } * ``` + * The column has to be a part of the `SELECT` fragment of a SQL statement. + * If [[yii\db\Query::select()|select()]] is used with an array in its parameter, Yii handles adding that required SQL fragment for you. * * @return $this the query object itself */ diff --git a/framework/db/Schema.php b/framework/db/Schema.php index e89aabe..120141d 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -21,17 +21,18 @@ use yii\caching\TagDependency; * * Schema represents the database schema information that is DBMS specific. * - * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the - * sequence object. This property is read-only. - * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only. - * @property string[] $schemaNames All schema names in the database, except system schemas. This property is + * @property-read string $lastInsertID The row ID of the last row inserted, or the last value retrieved from + * the sequence object. This property is read-only. + * @property-read QueryBuilder $queryBuilder The query builder for this connection. This property is * read-only. - * @property string $serverVersion Server version as a string. This property is read-only. - * @property string[] $tableNames All table names in the database. This property is read-only. - * @property TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element is an - * instance of [[TableSchema]] or its child class. This property is read-only. - * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. - * This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]], + * @property-read string[] $schemaNames All schema names in the database, except system schemas. This property + * is read-only. + * @property-read string $serverVersion Server version as a string. This property is read-only. + * @property-read string[] $tableNames All table names in the database. This property is read-only. + * @property-read TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element + * is an instance of [[TableSchema]] or its child class. This property is read-only. + * @property-write string $transactionIsolationLevel The transaction isolation level to use for this + * transaction. This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]], * [[Transaction::REPEATABLE_READ]] and [[Transaction::SERIALIZABLE]] but also a string containing DBMS specific * syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is write-only. * @@ -459,7 +460,7 @@ abstract class Schema extends BaseObject return $str; } - if (($value = $this->db->getSlavePdo()->quote($str)) !== false) { + if (mb_stripos($this->db->dsn, 'odbc:') === false && ($value = $this->db->getSlavePdo()->quote($str)) !== false) { return $value; } @@ -478,7 +479,11 @@ abstract class Schema extends BaseObject */ public function quoteTableName($name) { - if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { + + if (strpos($name, '(') === 0 && strpos($name, ')') === strlen($name) - 1) { + return $name; + } + if (strpos($name, '{{') !== false) { return $name; } if (strpos($name, '.') === false) { @@ -488,7 +493,6 @@ abstract class Schema extends BaseObject foreach ($parts as $i => $part) { $parts[$i] = $this->quoteSimpleTableName($part); } - return implode('.', $parts); } @@ -556,7 +560,7 @@ abstract class Schema extends BaseObject */ public function quoteSimpleColumnName($name) { - if (is_string($this->tableQuoteCharacter)) { + if (is_string($this->columnQuoteCharacter)) { $startingCharacter = $endingCharacter = $this->columnQuoteCharacter; } else { list($startingCharacter, $endingCharacter) = $this->columnQuoteCharacter; @@ -671,7 +675,7 @@ abstract class Schema extends BaseObject } $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql"; $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - return new $exceptionClass($message, $errorInfo, (int)$e->getCode(), $e); + return new $exceptionClass($message, $errorInfo, $e->getCode(), $e); } /** diff --git a/framework/db/SqlToken.php b/framework/db/SqlToken.php index 77497ee..0e947c1 100644 --- a/framework/db/SqlToken.php +++ b/framework/db/SqlToken.php @@ -13,10 +13,10 @@ use yii\base\BaseObject; * SqlToken represents SQL tokens produced by [[SqlTokenizer]] or its child classes. * * @property SqlToken[] $children Child tokens. - * @property bool $hasChildren Whether the token has children. This property is read-only. - * @property bool $isCollection Whether the token represents a collection of tokens. This property is + * @property-read bool $hasChildren Whether the token has children. This property is read-only. + * @property-read bool $isCollection Whether the token represents a collection of tokens. This property is * read-only. - * @property string $sql SQL code. This property is read-only. + * @property-read string $sql SQL code. This property is read-only. * * @author Sergey Makinen * @since 2.0.13 diff --git a/framework/db/TableSchema.php b/framework/db/TableSchema.php index 33e2c12..899ddb8 100644 --- a/framework/db/TableSchema.php +++ b/framework/db/TableSchema.php @@ -13,7 +13,7 @@ use yii\base\InvalidArgumentException; /** * TableSchema represents the metadata of a database table. * - * @property array $columnNames List of column names. This property is read-only. + * @property-read array $columnNames List of column names. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php index 3935a23..7a119d1 100644 --- a/framework/db/Transaction.php +++ b/framework/db/Transaction.php @@ -39,13 +39,13 @@ use yii\base\NotSupportedException; * > with PHP 5.x and PHP 7.x. `\Exception` implements the [`\Throwable` interface](https://secure.php.net/manual/en/class.throwable.php) * > since PHP 7.0, so you can skip the part with `\Exception` if your app uses only PHP 7.0 and higher. * - * @property bool $isActive Whether this transaction is active. Only an active transaction can [[commit()]] or - * [[rollBack()]]. This property is read-only. - * @property string $isolationLevel The transaction isolation level to use for this transaction. This can be - * one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but also a string - * containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is + * @property-read bool $isActive Whether this transaction is active. Only an active transaction can + * [[commit()]] or [[rollBack()]]. This property is read-only. + * @property-write string $isolationLevel The transaction isolation level to use for this transaction. This + * can be one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but also a + * string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is * write-only. - * @property int $level The current nesting level of the transaction. This property is read-only. + * @property-read int $level The current nesting level of the transaction. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -140,7 +140,10 @@ class Transaction extends \yii\base\BaseObject $schema = $this->db->getSchema(); if ($schema->supportsSavepoint()) { Yii::debug('Set savepoint ' . $this->_level, __METHOD__); - $schema->createSavepoint('LEVEL' . $this->_level); + // make sure the transaction wasn't autocommitted + if ($this->db->pdo->inTransaction()) { + $schema->createSavepoint('LEVEL' . $this->_level); + } } else { Yii::info('Transaction not started: nested transaction not supported', __METHOD__); throw new NotSupportedException('Transaction not started: nested transaction not supported.'); @@ -161,7 +164,10 @@ class Transaction extends \yii\base\BaseObject $this->_level--; if ($this->_level === 0) { Yii::debug('Commit transaction', __METHOD__); - $this->db->pdo->commit(); + // make sure the transaction wasn't autocommitted + if ($this->db->pdo->inTransaction()) { + $this->db->pdo->commit(); + } $this->db->trigger(Connection::EVENT_COMMIT_TRANSACTION); return; } @@ -169,7 +175,10 @@ class Transaction extends \yii\base\BaseObject $schema = $this->db->getSchema(); if ($schema->supportsSavepoint()) { Yii::debug('Release savepoint ' . $this->_level, __METHOD__); - $schema->releaseSavepoint('LEVEL' . $this->_level); + // make sure the transaction wasn't autocommitted + if ($this->db->pdo->inTransaction()) { + $schema->releaseSavepoint('LEVEL' . $this->_level); + } } else { Yii::info('Transaction not committed: nested transaction not supported', __METHOD__); } @@ -189,7 +198,10 @@ class Transaction extends \yii\base\BaseObject $this->_level--; if ($this->_level === 0) { Yii::debug('Roll back transaction', __METHOD__); - $this->db->pdo->rollBack(); + // make sure the transaction wasn't autocommitted + if ($this->db->pdo->inTransaction()) { + $this->db->pdo->rollBack(); + } $this->db->trigger(Connection::EVENT_ROLLBACK_TRANSACTION); return; } @@ -197,7 +209,10 @@ class Transaction extends \yii\base\BaseObject $schema = $this->db->getSchema(); if ($schema->supportsSavepoint()) { Yii::debug('Roll back to savepoint ' . $this->_level, __METHOD__); - $schema->rollBackSavepoint('LEVEL' . $this->_level); + // make sure the transaction wasn't autocommitted + if ($this->db->pdo->inTransaction()) { + $schema->rollBackSavepoint('LEVEL' . $this->_level); + } } else { Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__); } diff --git a/framework/db/conditions/InConditionBuilder.php b/framework/db/conditions/InConditionBuilder.php index 0424e03..f685216 100644 --- a/framework/db/conditions/InConditionBuilder.php +++ b/framework/db/conditions/InConditionBuilder.php @@ -33,7 +33,7 @@ class InConditionBuilder implements ExpressionBuilderInterface */ public function build(ExpressionInterface $expression, array &$params = []) { - $operator = $expression->getOperator(); + $operator = strtoupper($expression->getOperator()); $column = $expression->getColumn(); $values = $expression->getValues(); @@ -68,21 +68,36 @@ class InConditionBuilder implements ExpressionBuilderInterface } } + if (is_array($values)) { + $rawValues = $values; + } elseif ($values instanceof \Traversable) { + $rawValues = $this->getRawValuesFromTraversableObject($values); + } + + if (isset($rawValues) && in_array(null, $rawValues, true)) { + $nullCondition = $this->getNullCondition($operator, $column); + $nullConditionOperator = $operator === 'IN' ? 'OR' : 'AND'; + } + $sqlValues = $this->buildValues($expression, $values, $params); if (empty($sqlValues)) { - return $operator === 'IN' ? '0=1' : ''; + if (!isset($nullCondition)) { + return $operator === 'IN' ? '0=1' : ''; + } + return $nullCondition; } if (strpos($column, '(') === false) { $column = $this->queryBuilder->db->quoteColumnName($column); } if (count($sqlValues) > 1) { - return "$column $operator (" . implode(', ', $sqlValues) . ')'; + $sql = "$column $operator (" . implode(', ', $sqlValues) . ')'; + } else { + $operator = $operator === 'IN' ? '=' : '<>'; + $sql = $column . $operator . reset($sqlValues); } - $operator = $operator === 'IN' ? '=' : '<>'; - - return $column . $operator . reset($sqlValues); + return isset($nullCondition) ? sprintf('%s %s %s', $sql, $nullConditionOperator, $nullCondition) : $sql; } /** @@ -112,7 +127,7 @@ class InConditionBuilder implements ExpressionBuilderInterface $value = isset($value[$column]) ? $value[$column] : null; } if ($value === null) { - $sqlValues[$i] = 'NULL'; + continue; } elseif ($value instanceof ExpressionInterface) { $sqlValues[$i] = $this->queryBuilder->buildExpression($value, $params); } else { @@ -188,4 +203,39 @@ class InConditionBuilder implements ExpressionBuilderInterface return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')'; } + + /** + * Builds is null/is not null condition for column based on operator + * + * @param string $operator + * @param string $column + * @return string is null or is not null condition + * @since 2.0.31 + */ + protected function getNullCondition($operator, $column) { + $column = $this->queryBuilder->db->quoteColumnName($column); + if ($operator === 'IN') { + return sprintf('%s IS NULL', $column); + } + return sprintf('%s IS NOT NULL', $column); + } + + /** + * @param \Traversable $traversableObject + * @return array raw values + * @since 2.0.31 + */ + protected function getRawValuesFromTraversableObject(\Traversable $traversableObject) + { + $rawValues = []; + foreach ($traversableObject as $value) { + if (is_array($value)) { + $values = array_values($value); + $rawValues = array_merge($rawValues, $values); + } else { + $rawValues[] = $value; + } + } + return $rawValues; + } } diff --git a/framework/db/conditions/LikeConditionBuilder.php b/framework/db/conditions/LikeConditionBuilder.php index 62c3751..140e3e6 100644 --- a/framework/db/conditions/LikeConditionBuilder.php +++ b/framework/db/conditions/LikeConditionBuilder.php @@ -48,7 +48,7 @@ class LikeConditionBuilder implements ExpressionBuilderInterface */ public function build(ExpressionInterface $expression, array &$params = []) { - $operator = $expression->getOperator(); + $operator = strtoupper($expression->getOperator()); $column = $expression->getColumn(); $values = $expression->getValue(); $escape = $expression->getEscapingReplacements(); @@ -66,7 +66,9 @@ class LikeConditionBuilder implements ExpressionBuilderInterface return $not ? '' : '0=1'; } - if (strpos($column, '(') === false) { + if ($column instanceof ExpressionInterface) { + $column = $this->queryBuilder->buildExpression($column, $params); + } elseif (is_string($column) && strpos($column, '(') === false) { $column = $this->queryBuilder->db->quoteColumnName($column); } diff --git a/framework/db/mssql/ColumnSchema.php b/framework/db/mssql/ColumnSchema.php index 8ccd69f..78895d6 100644 --- a/framework/db/mssql/ColumnSchema.php +++ b/framework/db/mssql/ColumnSchema.php @@ -15,6 +15,13 @@ namespace yii\db\mssql; class ColumnSchema extends \yii\db\ColumnSchema { /** + * @var bool whether this column is a computed column + * @since 2.0.39 + */ + public $isComputed; + + + /** * Prepares default value and converts it according to [[phpType]] * @param mixed $value default value * @return mixed converted value diff --git a/framework/db/mssql/DBLibPDO.php b/framework/db/mssql/DBLibPDO.php new file mode 100644 index 0000000..4e53bb3 --- /dev/null +++ b/framework/db/mssql/DBLibPDO.php @@ -0,0 +1,51 @@ + + * @since 2.0 + */ +class DBLibPDO extends \PDO +{ + /** + * Returns value of the last inserted ID. + * @param string|null $sequence the sequence name. Defaults to null. + * @return int last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return $this->query('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS bigint)')->fetchColumn(); + } + + /** + * Retrieve a database connection attribute. + * + * It is necessary to override PDO's method as some MSSQL PDO driver (e.g. dblib) does not + * support getting attributes. + * @param int $attribute One of the PDO::ATTR_* constants. + * @return mixed A successful call returns the value of the requested PDO attribute. + * An unsuccessful call returns null. + */ + public function getAttribute($attribute) + { + try { + return parent::getAttribute($attribute); + } catch (\PDOException $e) { + switch ($attribute) { + case self::ATTR_SERVER_VERSION: + return $this->query("SELECT CAST(SERVERPROPERTY('productversion') AS VARCHAR)")->fetchColumn(); + default: + throw $e; + } + } + } +} diff --git a/framework/db/mssql/QueryBuilder.php b/framework/db/mssql/QueryBuilder.php index 350a9c8..da04e1d 100644 --- a/framework/db/mssql/QueryBuilder.php +++ b/framework/db/mssql/QueryBuilder.php @@ -8,6 +8,7 @@ namespace yii\db\mssql; use yii\base\InvalidArgumentException; +use yii\base\NotSupportedException; use yii\db\Constraint; use yii\db\Expression; @@ -121,6 +122,9 @@ class QueryBuilder extends \yii\db\QueryBuilder $sql = preg_replace('/^([\s(])*SELECT(\s+DISTINCT)?(?!\s*TOP\s*\()/i', "\\1SELECT\\2 rowNum = ROW_NUMBER() over ($orderBy),", $sql); if ($this->hasLimit($limit)) { + if ($limit instanceof Expression) { + $limit = '('. (string)$limit . ')'; + } $sql = "SELECT TOP $limit * FROM ($sql) sub"; } else { $sql = "SELECT * FROM ($sql) sub"; @@ -166,15 +170,41 @@ class QueryBuilder extends \yii\db\QueryBuilder * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. * @return string the SQL statement for changing the definition of a column. + * @throws NotSupportedException if this is not supported by the underlying DBMS. */ public function alterColumn($table, $column, $type) { + $sqlAfter = []; + + $columnName = $this->db->quoteColumnName($column); + $tableName = $this->db->quoteTableName($table); + + $constraintBase = preg_replace('/[^a-z0-9_]/i', '', $table . '_' . $column); + $type = $this->getColumnType($type); - $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN ' - . $this->db->quoteColumnName($column) . ' ' - . $this->getColumnType($type); - return $sql; + if (preg_match('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', $type, $matches)) { + $type = preg_replace('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', '', $type); + $sqlAfter[] = $this->dropConstraintsForColumn($table, $column, 'D'); + $sqlAfter[] = $this->addDefaultValue("DF_{$constraintBase}", $table, $column, $matches[1]); + } else { + $sqlAfter[] = $this->dropConstraintsForColumn($table, $column, 'D'); + } + + if (preg_match('/\s+CHECK\s+\((.+)\)/i', $type, $matches)) { + $type = preg_replace('/\s+CHECK\s+\((.+)\)/i', '', $type); + $sqlAfter[] = "ALTER TABLE {$tableName} ADD CONSTRAINT " . $this->db->quoteColumnName("CK_{$constraintBase}") . " CHECK ({$matches[1]})"; + } + + $type = preg_replace('/\s+UNIQUE/i', '', $type, -1, $count); + if ($count) { + $sqlAfter[] = "ALTER TABLE {$tableName} ADD CONSTRAINT " . $this->db->quoteColumnName("UQ_{$constraintBase}") . " UNIQUE ({$columnName})"; + } + + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN ' + . $this->db->quoteColumnName($column) . ' ' + . $this->getColumnType($type) . "\n" + . implode("\n", $sqlAfter); } /** @@ -426,7 +456,8 @@ class QueryBuilder extends \yii\db\QueryBuilder // @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); - $columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", $params); + // @see https://github.com/yiisoft/yii2/issues/12599 + $columns[$name] = new Expression("CONVERT(VARBINARY(MAX), $phName)", $params); } } } @@ -436,10 +467,46 @@ class QueryBuilder extends \yii\db\QueryBuilder /** * {@inheritdoc} + * Added OUTPUT construction for getting inserted data (for SQL Server 2005 or later) + * OUTPUT clause - The OUTPUT clause is new to SQL Server 2005 and has the ability to access + * the INSERTED and DELETED tables as is the case with a trigger. */ public function insert($table, $columns, &$params) { - return parent::insert($table, $this->normalizeTableRowData($table, $columns, $params), $params); + $columns = $this->normalizeTableRowData($table, $columns, $params); + + $version2005orLater = version_compare($this->db->getSchema()->getServerVersion(), '9', '>='); + + list($names, $placeholders, $values, $params) = $this->prepareInsertValues($table, $columns, $params); + if ($version2005orLater) { + $schema = $this->db->getTableSchema($table); + $cols = []; + $columns = []; + foreach ($schema->columns as $column) { + if ($column->isComputed) { + continue; + } + $quoteColumnName = $this->db->quoteColumnName($column->name); + $cols[] = $quoteColumnName . ' ' + . $column->dbType + . (in_array($column->dbType, ['char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary']) ? "(MAX)" : "") + . ' ' . ($column->allowNull ? "NULL" : ""); + $columns[] = 'INSERTED.' . $quoteColumnName; + } + } + $countColumns = count($columns); + + $sql = 'INSERT INTO ' . $this->db->quoteTableName($table) + . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '') + . (($version2005orLater && $countColumns) ? ' OUTPUT ' . implode(',', $columns) . ' INTO @temporary_inserted' : '') + . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values); + + if ($version2005orLater && $countColumns) { + $sql = 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE (' . implode(', ', $cols) . ');' . $sql . + ';SELECT * FROM @temporary_inserted'; + } + + return $sql; } /** @@ -471,8 +538,18 @@ class QueryBuilder extends \yii\db\QueryBuilder } $on = $this->buildCondition($onCondition, $params); list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params); + + /** + * Fix number of select query params for old MSSQL version that does not support offset correctly. + * @see QueryBuilder::oldBuildOrderByAndLimit + */ + $insertNamesUsing = $insertNames; + if (strstr($values, 'rowNum = ROW_NUMBER()') !== false) { + $insertNamesUsing = array_merge(['[rowNum]'], $insertNames); + } + $mergeSql = 'MERGE ' . $this->db->quoteTableName($table) . ' WITH (HOLDLOCK) ' - . 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') ' + . 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNamesUsing) . ') ' . "ON ($on)"; $insertValues = []; foreach ($insertNames as $name) { @@ -535,4 +612,50 @@ class QueryBuilder extends \yii\db\QueryBuilder return parent::extractAlias($table); } + + /** + * Builds a SQL statement for dropping constraints for column of table. + * + * @param string $table the table whose constraint is to be dropped. The name will be properly quoted by the method. + * @param string $column the column whose constraint is to be dropped. The name will be properly quoted by the method. + * @param string $type type of constraint, leave empty for all type of constraints(for example: D - default, 'UQ' - unique, 'C' - check) + * @see https://docs.microsoft.com/sql/relational-databases/system-catalog-views/sys-objects-transact-sql + * @return string the DROP CONSTRAINTS SQL + */ + private function dropConstraintsForColumn($table, $column, $type='') + { + return "DECLARE @tableName VARCHAR(MAX) = '" . $this->db->quoteTableName($table) . "' +DECLARE @columnName VARCHAR(MAX) = '{$column}' + +WHILE 1=1 BEGIN + DECLARE @constraintName NVARCHAR(128) + SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id]) + FROM ( + SELECT sc.[constid] object_id + FROM [sys].[sysconstraints] sc + JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName + WHERE sc.[id] = OBJECT_ID(@tableName) + UNION + SELECT object_id(i.[name]) FROM [sys].[indexes] i + JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName + JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id] + WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName) + ) cons + JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id] + " . (!empty($type) ? " WHERE so.[type]='{$type}'" : "") . ") + IF @constraintName IS NULL BREAK + EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']') +END"; + } + + /** + * Drop all constraints before column delete + * {@inheritdoc} + */ + public function dropColumn($table, $column) + { + return $this->dropConstraintsForColumn($table, $column) . "\nALTER TABLE " . $this->db->quoteTableName($table) + . " DROP COLUMN " . $this->db->quoteColumnName($column); + } + } diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php index 6e86cfb..365d96e 100644 --- a/framework/db/mssql/Schema.php +++ b/framework/db/mssql/Schema.php @@ -180,12 +180,7 @@ FROM [INFORMATION_SCHEMA].[TABLES] AS [t] WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW') ORDER BY [t].[table_name] SQL; - $tables = $this->db->createCommand($sql, [':schema' => $schema])->queryColumn(); - $tables = array_map(static function ($item) { - return '[' . $item . ']'; - }, $tables); - - return $tables; + return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn(); } /** @@ -207,6 +202,29 @@ SQL; /** * {@inheritdoc} */ + protected function getSchemaMetadata($schema, $type, $refresh) + { + $metadata = []; + $methodName = 'getTable' . ucfirst($type); + $tableNames = array_map(function ($table) { + return $this->quoteSimpleTableName($table); + }, $this->getTableNames($schema, $refresh)); + foreach ($tableNames as $name) { + if ($schema !== '') { + $name = $schema . '.' . $name; + } + $tableMetadata = $this->$methodName($name, $refresh); + if ($tableMetadata !== null) { + $metadata[] = $tableMetadata; + } + } + + return $metadata; + } + + /** + * {@inheritdoc} + */ protected function loadTablePrimaryKey($tableName) { return $this->loadTableConstraints($tableName, 'primaryKey'); @@ -364,6 +382,7 @@ SQL; $column->enumValues = []; // mssql has only vague equivalents to enum $column->isPrimaryKey = null; // primary key will be determined in findColumns() method $column->autoIncrement = $info['is_identity'] == 1; + $column->isComputed = (bool)$info['is_computed']; $column->unsigned = stripos($column->dbType, 'unsigned') !== false; $column->comment = $info['comment'] === null ? '' : $info['comment']; @@ -436,6 +455,7 @@ SELECT END AS 'data_type', [t1].[column_default], COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity, + COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsComputed') AS is_computed, ( SELECT CONVERT(VARCHAR, [t2].[value]) FROM [sys].[extended_properties] AS [t2] @@ -751,4 +771,37 @@ SQL; return parent::quoteColumnName($name); } + + /** + * Retrieving inserted data from a primary key request of type uniqueidentifier (for SQL Server 2005 or later) + * {@inheritdoc} + */ + public function insert($table, $columns) + { + $command = $this->db->createCommand()->insert($table, $columns); + if (!$command->execute()) { + return false; + } + + $isVersion2005orLater = version_compare($this->db->getSchema()->getServerVersion(), '9', '>='); + $inserted = $isVersion2005orLater ? $command->pdoStatement->fetch() : []; + + $tableSchema = $this->getTableSchema($table); + $result = []; + foreach ($tableSchema->primaryKey as $name) { + // @see https://github.com/yiisoft/yii2/issues/13828 & https://github.com/yiisoft/yii2/issues/17474 + if (isset($inserted[$name])) { + $result[$name] = $inserted[$name]; + } elseif ($tableSchema->columns[$name]->autoIncrement) { + // for a version earlier than 2005 + $result[$name] = $this->getLastInsertID($tableSchema->sequenceName); + } elseif (isset($columns[$name])) { + $result[$name] = $columns[$name]; + } else { + $result[$name] = $tableSchema->columns[$name]->defaultValue; + } + } + + return $result; + } } diff --git a/framework/db/mysql/ColumnSchemaBuilder.php b/framework/db/mysql/ColumnSchemaBuilder.php index 02cef57..de5740d 100644 --- a/framework/db/mysql/ColumnSchemaBuilder.php +++ b/framework/db/mysql/ColumnSchemaBuilder.php @@ -61,10 +61,10 @@ class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder $format = '{type}{length}{comment}{check}{append}{pos}'; break; case self::CATEGORY_NUMERIC: - $format = '{type}{length}{unsigned}{notnull}{unique}{default}{comment}{check}{append}{pos}'; + $format = '{type}{length}{unsigned}{notnull}{default}{unique}{comment}{append}{pos}{check}'; break; default: - $format = '{type}{length}{notnull}{unique}{default}{comment}{check}{append}{pos}'; + $format = '{type}{length}{notnull}{default}{unique}{comment}{append}{pos}{check}'; } return $this->buildCompleteString($format); diff --git a/framework/db/mysql/QueryBuilder.php b/framework/db/mysql/QueryBuilder.php index 14d729b..6ef2f30 100644 --- a/framework/db/mysql/QueryBuilder.php +++ b/framework/db/mysql/QueryBuilder.php @@ -9,6 +9,8 @@ namespace yii\db\mysql; use yii\base\InvalidArgumentException; use yii\base\NotSupportedException; +use yii\caching\CacheInterface; +use yii\caching\DbCache; use yii\db\Exception; use yii\db\Expression; use yii\db\Query; @@ -88,7 +90,7 @@ class QueryBuilder extends \yii\db\QueryBuilder $row = array_values($row); $sql = $row[1]; } - if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) { + if (preg_match_all('/^\s*[`"](.*?)[`"]\s+(.*?),?$/m', $sql, $matches)) { foreach ($matches[1] as $i => $c) { if ($c === $oldName) { return "ALTER TABLE $quotedTable CHANGE " @@ -371,7 +373,7 @@ class QueryBuilder extends \yii\db\QueryBuilder $row = array_values($row); $sql = $row[1]; } - if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) { + if (preg_match_all('/^\s*[`"](.*?)[`"]\s+(.*?),?$/m', $sql, $matches)) { foreach ($matches[1] as $i => $c) { if ($c === $column) { return $matches[2][$i]; @@ -390,7 +392,23 @@ class QueryBuilder extends \yii\db\QueryBuilder */ private function supportsFractionalSeconds() { - $version = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION); + // use cache to prevent opening MySQL connection + // https://github.com/yiisoft/yii2/issues/13749#issuecomment-481657224 + $key = [__METHOD__, $this->db->dsn]; + $cache = null; + $schemaCache = (\Yii::$app && is_string($this->db->schemaCache)) ? \Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache; + // If the `$schemaCache` is an instance of `DbCache` we don't use it to avoid a loop + if ($this->db->enableSchemaCache && $schemaCache instanceof CacheInterface && !($schemaCache instanceof DbCache)) { + $cache = $schemaCache; + } + $version = $cache ? $cache->get($key) : null; + if (!$version) { + $version = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION); + if ($cache) { + $cache->set($key, $version, $this->db->schemaCacheDuration); + } + } + return version_compare($version, '5.6.4', '>='); } diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php index 95f9a47..90ecd31 100644 --- a/framework/db/mysql/Schema.php +++ b/framework/db/mysql/Schema.php @@ -293,9 +293,9 @@ SQL; * * See details here: https://mariadb.com/kb/en/library/now/#description */ - if (($column->type === 'timestamp' || $column->type === 'datetime') && - ($info['default'] === 'CURRENT_TIMESTAMP' || $info['default'] === 'current_timestamp()')) { - $column->defaultValue = new Expression('CURRENT_TIMESTAMP'); + if (($column->type === 'timestamp' || $column->type === 'datetime') + && preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', $info['default'], $matches)) { + $column->defaultValue = new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : '')); } elseif (isset($type) && $type === 'bit') { $column->defaultValue = bindec(trim($info['default'], 'b\'')); } else { @@ -370,20 +370,20 @@ SQL; { $sql = <<<'SQL' SELECT - kcu.constraint_name, - kcu.column_name, - kcu.referenced_table_name, - kcu.referenced_column_name -FROM information_schema.referential_constraints AS rc -JOIN information_schema.key_column_usage AS kcu ON + `kcu`.`CONSTRAINT_NAME` AS `constraint_name`, + `kcu`.`COLUMN_NAME` AS `column_name`, + `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`, + `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name` +FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc` +JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON ( - kcu.constraint_catalog = rc.constraint_catalog OR - (kcu.constraint_catalog IS NULL AND rc.constraint_catalog IS NULL) + `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR + (`kcu`.`CONSTRAINT_CATALOG` IS NULL AND `rc`.`CONSTRAINT_CATALOG` IS NULL) ) AND - kcu.constraint_schema = rc.constraint_schema AND - kcu.constraint_name = rc.constraint_name -WHERE rc.constraint_schema = database() AND kcu.table_schema = database() -AND rc.table_name = :tableName AND kcu.table_name = :tableName1 + `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND + `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME` +WHERE `rc`.`CONSTRAINT_SCHEMA` = database() AND `kcu`.`TABLE_SCHEMA` = database() +AND `rc`.`TABLE_NAME` = :tableName AND `kcu`.`TABLE_NAME` = :tableName1 SQL; try { @@ -413,9 +413,9 @@ SQL; $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi'; if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { - $fks = array_map('trim', explode(',', str_replace('`', '', $match[1]))); - $pks = array_map('trim', explode(',', str_replace('`', '', $match[3]))); - $constraint = [str_replace('`', '', $match[2])]; + $fks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[1]))); + $pks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[3]))); + $constraint = [str_replace(['`', '"'], '', $match[2])]; foreach ($fks as $k => $name) { $constraint[$name] = $pks[$k]; } @@ -446,11 +446,11 @@ SQL; $sql = $this->getCreateTableSql($table); $uniqueIndexes = []; - $regexp = '/UNIQUE KEY\s+\`(.+)\`\s*\((\`.+\`)+\)/mi'; + $regexp = '/UNIQUE KEY\s+[`"](.+)[`"]\s*\(([`"].+[`"])+\)/mi'; if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $indexName = $match[1]; - $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`'))); + $indexColumns = array_map('trim', preg_split('/[`"],[`"]/', trim($match[2], '`"'))); $uniqueIndexes[$indexName] = $indexColumns; } } @@ -512,9 +512,9 @@ FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`, `information_schema`.`TABLE_CONSTRAINTS` AS `tc` WHERE - `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName - AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` - AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY' + `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName1, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName + AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName1 AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` + AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName2 AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY' UNION SELECT `kcu`.`CONSTRAINT_NAME` AS `name`, @@ -530,15 +530,21 @@ FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`, `information_schema`.`TABLE_CONSTRAINTS` AS `tc` WHERE - `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName - AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE') + `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName2, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName3 + AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName4 AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE') ORDER BY `position` ASC SQL; $resolvedName = $this->resolveTableName($tableName); $constraints = $this->db->createCommand($sql, [ ':schemaName' => $resolvedName->schemaName, + ':schemaName1' => $resolvedName->schemaName, + ':schemaName2' => $resolvedName->schemaName, ':tableName' => $resolvedName->name, + ':tableName1' => $resolvedName->name, + ':tableName2' => $resolvedName->name, + ':tableName3' => $resolvedName->name, + ':tableName4' => $resolvedName->name ])->queryAll(); $constraints = $this->normalizePdoRowKeyCase($constraints, true); $constraints = ArrayHelper::index($constraints, null, ['type', 'name']); diff --git a/framework/db/oci/Command.php b/framework/db/oci/Command.php new file mode 100644 index 0000000..8f7edc6 --- /dev/null +++ b/framework/db/oci/Command.php @@ -0,0 +1,35 @@ +pendingParams as $name => $value) { + if (\PDO::PARAM_STR === $value[1]) { + $paramsPassedByReference[$name] = $value[0]; + $this->pdoStatement->bindParam($name, $paramsPassedByReference[$name], $value[1], strlen($value[0])); + } else { + $this->pdoStatement->bindValue($name, $value[0], $value[1]); + } + } + $this->pendingParams = []; + } +} diff --git a/framework/db/oci/Schema.php b/framework/db/oci/Schema.php index c240e8e..7b1cc80 100644 --- a/framework/db/oci/Schema.php +++ b/framework/db/oci/Schema.php @@ -24,8 +24,8 @@ use yii\helpers\ArrayHelper; /** * Schema is the class for retrieving metadata from an Oracle database. * - * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the - * sequence object. This property is read-only. + * @property-read string $lastInsertID The row ID of the last row inserted, or the last value retrieved from + * the sequence object. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -419,10 +419,12 @@ SQL; $c->defaultValue = new Expression('CURRENT_TIMESTAMP'); } else { if ($defaultValue !== null) { - if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'" - && $defaultValue[$len - 1] === "'" + if ( + strlen($defaultValue) > 2 + && strpos($defaultValue, "'") === 0 + && substr($defaultValue, -1) === "'" ) { - $defaultValue = substr($column['DATA_DEFAULT'], 1, -1); + $defaultValue = substr($defaultValue, 1, -1); } else { $defaultValue = trim($defaultValue); } @@ -614,7 +616,7 @@ SQL; $phName = QueryBuilder::PARAM_PREFIX . (count($params) + count($returnParams)); $returnParams[$phName] = [ 'column' => $name, - 'value' => null, + 'value' => '', ]; if (!isset($columnSchemas[$name]) || $columnSchemas[$name]->phpType !== 'integer') { $returnParams[$phName]['dataType'] = \PDO::PARAM_STR; diff --git a/framework/db/pgsql/ColumnSchema.php b/framework/db/pgsql/ColumnSchema.php index 25490e4..b93ad3c 100644 --- a/framework/db/pgsql/ColumnSchema.php +++ b/framework/db/pgsql/ColumnSchema.php @@ -49,6 +49,11 @@ class ColumnSchema extends \yii\db\ColumnSchema * @deprecated Since 2.0.14.1 and will be removed in 2.1. */ public $deserializeArrayColumnToArrayExpression = true; + /** + * @var string name of associated sequence if column is auto-incremental + * @since 2.0.29 + */ + public $sequenceName; /** diff --git a/framework/db/pgsql/QueryBuilder.php b/framework/db/pgsql/QueryBuilder.php index 2f07200..fa546c6 100644 --- a/framework/db/pgsql/QueryBuilder.php +++ b/framework/db/pgsql/QueryBuilder.php @@ -263,8 +263,8 @@ class QueryBuilder extends \yii\db\QueryBuilder $multiAlterStatement = []; $constraintPrefix = preg_replace('/[^a-z0-9_]/i', '', $table . '_' . $column); - if (preg_match('/\s+DEFAULT\s+(["\']?\w+["\']?)/i', $type, $matches)) { - $type = preg_replace('/\s+DEFAULT\s+(["\']?\w+["\']?)/i', '', $type); + if (preg_match('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', $type, $matches)) { + $type = preg_replace('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', '', $type); $multiAlterStatement[] = "ALTER COLUMN {$columnName} SET DEFAULT {$matches[1]}"; } else { // safe to drop default even if there was none in the first place diff --git a/framework/db/pgsql/Schema.php b/framework/db/pgsql/Schema.php index 462e8ba..3b70383 100644 --- a/framework/db/pgsql/Schema.php +++ b/framework/db/pgsql/Schema.php @@ -176,7 +176,7 @@ SQL; SELECT c.relname AS table_name FROM pg_class c INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace -WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f') +WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p') ORDER BY c.relname SQL; return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn(); @@ -232,7 +232,7 @@ INNER JOIN "pg_index" AS "i" INNER JOIN "pg_class" AS "ic" ON "ic"."oid" = "i"."indexrelid" INNER JOIN "pg_attribute" AS "ia" - ON "ia"."attrelid" = "i"."indrelid" AND "ia"."attnum" = ANY ("i"."indkey") + ON "ia"."attrelid" = "i"."indexrelid" WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName ORDER BY "ia"."attnum" ASC SQL; @@ -443,7 +443,7 @@ SQL; $row = array_change_key_case($row, CASE_LOWER); } $column = $row['columnname']; - if (!empty($column) && $column[0] === '"') { + if (strpos($column, '"') === 0) { // postgres will quote names that are not lowercase-only // https://github.com/yiisoft/yii2/issues/10613 $column = substr($column, 1, -1); @@ -463,6 +463,12 @@ SQL; { $tableName = $this->db->quoteValue($table->name); $schemaName = $this->db->quoteValue($table->schemaName); + + $orIdentity = ''; + if (version_compare($this->db->serverVersion, '12.0', '>=')) { + $orIdentity = 'OR attidentity != \'\''; + } + $sql = << 0 AND t.typname != '' + a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped AND c.relname = {$tableName} AND d.nspname = {$schemaName} ORDER BY @@ -536,17 +543,26 @@ SQL; $table->columns[$column->name] = $column; if ($column->isPrimaryKey) { $table->primaryKey[] = $column->name; - if ($table->sequenceName === null && preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) { - $table->sequenceName = preg_replace(['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'], '', $column->defaultValue); + if ($table->sequenceName === null) { + $table->sequenceName = $column->sequenceName; } $column->defaultValue = null; } elseif ($column->defaultValue) { - if ($column->type === 'timestamp' && $column->defaultValue === 'now()') { + if ( + in_array($column->type, [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME], true) && + in_array( + strtoupper($column->defaultValue), + ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'], + true + ) + ) { $column->defaultValue = new Expression($column->defaultValue); } elseif ($column->type === 'boolean') { $column->defaultValue = ($column->defaultValue === 'true'); - } elseif (strncasecmp($column->dbType, 'bit', 3) === 0 || strncasecmp($column->dbType, 'varbit', 6) === 0) { - $column->defaultValue = bindec(trim($column->defaultValue, 'B\'')); + } elseif (preg_match("/^B'(.*?)'::/", $column->defaultValue, $matches)) { + $column->defaultValue = bindec($matches[1]); + } elseif (preg_match("/^'(\d+)'::\"bit\"$/", $column->defaultValue, $matches)) { + $column->defaultValue = bindec($matches[1]); } elseif (preg_match("/^'(.*?)'::/", $column->defaultValue, $matches)) { $column->defaultValue = $column->phpTypecast($matches[1]); } elseif (preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $column->defaultValue, $matches)) { @@ -586,6 +602,13 @@ SQL; $column->scale = $info['numeric_scale']; $column->size = $info['size'] === null ? null : (int) $info['size']; $column->dimension = (int)$info['dimension']; + // pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null default value + if (isset($column->defaultValue) && preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) { + $column->sequenceName = preg_replace(['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'], '', $column->defaultValue); + } elseif (isset($info['sequence_name'])) { + $column->sequenceName = $this->resolveTableName($info['sequence_name'])->fullName; + } + if (isset($this->typeMap[$column->dbType])) { $column->type = $this->typeMap[$column->dbType]; } else { @@ -641,7 +664,7 @@ SELECT "fa"."attname" AS "foreign_column_name", "c"."confupdtype" AS "on_update", "c"."confdeltype" AS "on_delete", - "c"."consrc" AS "check_expr" + pg_get_constraintdef("c"."oid") AS "check_expr" FROM "pg_class" AS "tc" INNER JOIN "pg_namespace" AS "tcns" ON "tcns"."oid" = "tc"."relnamespace" diff --git a/framework/db/sqlite/QueryBuilder.php b/framework/db/sqlite/QueryBuilder.php index 38e0fa9..dcf3dfc 100644 --- a/framework/db/sqlite/QueryBuilder.php +++ b/framework/db/sqlite/QueryBuilder.php @@ -29,9 +29,9 @@ class QueryBuilder extends \yii\db\QueryBuilder */ public $typeMap = [ Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', - Schema::TYPE_UPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL', + Schema::TYPE_UPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', - Schema::TYPE_UBIGPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL', + Schema::TYPE_UBIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', Schema::TYPE_CHAR => 'char(1)', Schema::TYPE_STRING => 'varchar(255)', Schema::TYPE_TEXT => 'text', @@ -520,6 +520,11 @@ class QueryBuilder extends \yii\db\QueryBuilder $sql = "$sql{$this->separator}$union"; } + $with = $this->buildWithQueries($query->withQueries, $params); + if ($with !== '') { + $sql = "$with{$this->separator}$sql"; + } + return [$sql, $params]; } @@ -545,4 +550,22 @@ class QueryBuilder extends \yii\db\QueryBuilder return trim($result); } + + /** + * {@inheritdoc} + */ + public function createIndex($name, $table, $columns, $unique = false) + { + $tableParts = explode('.', $table); + + $schema = null; + if (count($tableParts) === 2) { + list ($schema, $table) = $tableParts; + } + + return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ') + . $this->db->quoteTableName(($schema ? $schema . '.' : '') . $name) . ' ON ' + . $this->db->quoteTableName($table) + . ' (' . $this->buildColumns($columns) . ')'; + } } diff --git a/framework/db/sqlite/Schema.php b/framework/db/sqlite/Schema.php index c8898d8..0b5a0cc 100644 --- a/framework/db/sqlite/Schema.php +++ b/framework/db/sqlite/Schema.php @@ -24,8 +24,9 @@ use yii\helpers\ArrayHelper; /** * Schema is the class for retrieving metadata from a SQLite (2/3) database. * - * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. - * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]]. + * @property-write string $transactionIsolationLevel The transaction isolation level to use for this + * transaction. This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]]. This + * property is write-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/di/Container.php b/framework/di/Container.php index e57eadd..e527edb 100644 --- a/framework/di/Container.php +++ b/framework/di/Container.php @@ -8,6 +8,9 @@ namespace yii\di; use ReflectionClass; +use ReflectionException; +use ReflectionNamedType; +use ReflectionParameter; use Yii; use yii\base\Component; use yii\base\InvalidConfigException; @@ -91,8 +94,10 @@ use yii\helpers\ArrayHelper; * * For more details and usage information on Container, see the [guide article on di-containers](guide:concept-di-container). * - * @property array $definitions The list of the object definitions or the loaded shared objects (type or ID => - * definition or instance). This property is read-only. + * @property-read array $definitions The list of the object definitions or the loaded shared objects (type or + * ID => definition or instance). This property is read-only. + * @property-write bool $resolveArrays Whether to attempt to resolve elements in array dependencies. This + * property is write-only. * * @author Qiang Xue * @since 2.0 @@ -120,6 +125,10 @@ class Container extends Component * is associated with a list of constructor parameter types or default values. */ private $_dependencies = []; + /** + * @var bool whether to attempt to resolve elements in array dependencies + */ + private $_resolveArrays = false; /** @@ -137,11 +146,14 @@ class Container extends Component * In this case, the constructor parameters and object configurations will be used * only if the class is instantiated the first time. * - * @param string $class the class name or an alias name (e.g. `foo`) that was previously registered via [[set()]] - * or [[setSingleton()]]. - * @param array $params a list of constructor parameter values. The parameters should be provided in the order - * they appear in the constructor declaration. If you want to skip some parameters, you should index the remaining - * ones with the integers that represent their positions in the constructor parameter list. + * @param string|Instance $class the class Instance, name or an alias name (e.g. `foo`) that was previously + * registered via [[set()]] or [[setSingleton()]]. + * @param array $params a list of constructor parameter values. Use one of two definitions: + * - Parameters as name-value pairs, for example: `['posts' => PostRepository::class]`. + * - Parameters in the order they appear in the constructor declaration. If you want to skip some parameters, + * you should index the remaining ones with the integers that represent their positions in the constructor + * parameter list. + * Dependencies indexed by name and by position in the same array are not allowed. * @param array $config a list of name-value pairs that will be used to initialize the object properties. * @return object an instance of the requested class. * @throws InvalidConfigException if the class cannot be recognized or correspond to an invalid definition @@ -149,6 +161,9 @@ class Container extends Component */ public function get($class, $params = [], $config = []) { + if ($class instanceof Instance) { + $class = $class->id; + } if (isset($this->_singletons[$class])) { // singleton return $this->_singletons[$class]; @@ -323,9 +338,15 @@ class Container extends Component return ['class' => $class]; } elseif (is_string($definition)) { return ['class' => $definition]; + } elseif ($definition instanceof Instance) { + return ['class' => $definition->id]; } elseif (is_callable($definition, true) || is_object($definition)) { return $definition; } elseif (is_array($definition)) { + if (!isset($definition['class']) && isset($definition['__class'])) { + $definition['class'] = $definition['__class']; + unset($definition['__class']); + } if (!isset($definition['class'])) { if (strpos($class, '\\') !== false) { $definition['class'] = $class; @@ -364,8 +385,23 @@ class Container extends Component /* @var $reflection ReflectionClass */ list($reflection, $dependencies) = $this->getDependencies($class); + $addDependencies = []; + if (isset($config['__construct()'])) { + $addDependencies = $config['__construct()']; + unset($config['__construct()']); + } foreach ($params as $index => $param) { - $dependencies[$index] = $param; + $addDependencies[$index] = $param; + } + + $this->validateDependencies($addDependencies); + + if ($addDependencies && is_int(key($addDependencies))) { + $dependencies = array_values($dependencies); + $dependencies = $this->mergeDependencies($dependencies, $addDependencies); + } else { + $dependencies = $this->mergeDependencies($dependencies, $addDependencies); + $dependencies = array_values($dependencies); } $dependencies = $this->resolveDependencies($dependencies, $reflection); @@ -393,6 +429,47 @@ class Container extends Component } /** + * @param array $a + * @param array $b + * @return array + */ + private function mergeDependencies($a, $b) + { + foreach ($b as $index => $dependency) { + $a[$index] = $dependency; + } + return $a; + } + + /** + * @param array $parameters + * @throws InvalidConfigException + */ + private function validateDependencies($parameters) + { + $hasStringParameter = false; + $hasIntParameter = false; + foreach ($parameters as $index => $parameter) { + if (is_string($index)) { + $hasStringParameter = true; + if ($hasIntParameter) { + break; + } + } else { + $hasIntParameter = true; + if ($hasStringParameter) { + break; + } + } + } + if ($hasIntParameter && $hasStringParameter) { + throw new InvalidConfigException( + 'Dependencies indexed by name and by position in the same array are not allowed.' + ); + } + } + + /** * Merges the user-specified constructor parameters with the ones registered via [[set()]]. * @param string $class class name, interface name or alias name * @param array $params the constructor parameters @@ -418,7 +495,7 @@ class Container extends Component * Returns the dependencies of the specified class. * @param string $class class name, interface name or alias name * @return array the dependencies of the specified class. - * @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled. + * @throws NotInstantiableException if a dependency cannot be resolved or if a dependency cannot be fulfilled. */ protected function getDependencies($class) { @@ -430,19 +507,54 @@ class Container extends Component try { $reflection = new ReflectionClass($class); } catch (\ReflectionException $e) { - throw new InvalidConfigException('Failed to instantiate component or class "' . $class . '".', 0, $e); + throw new NotInstantiableException( + $class, + 'Failed to instantiate component or class "' . $class . '".', + 0, + $e + ); } $constructor = $reflection->getConstructor(); if ($constructor !== null) { foreach ($constructor->getParameters() as $param) { - if (version_compare(PHP_VERSION, '5.6.0', '>=') && $param->isVariadic()) { + if (PHP_VERSION_ID >= 50600 && $param->isVariadic()) { break; - } elseif ($param->isDefaultValueAvailable()) { - $dependencies[] = $param->getDefaultValue(); + } + + if (PHP_VERSION_ID >= 80000) { + $c = $param->getType(); + $isClass = $c !== null && !$param->getType()->isBuiltin(); + } else { + try { + $c = $param->getClass(); + } catch (ReflectionException $e) { + if (!$this->isNulledParam($param)) { + $notInstantiableClass = null; + if (PHP_VERSION_ID >= 70000) { + $type = $param->getType(); + if ($type instanceof ReflectionNamedType) { + $notInstantiableClass = $type->getName(); + } + } + throw new NotInstantiableException( + $notInstantiableClass, + $notInstantiableClass === null ? 'Can not instantiate unknown class.' : null + ); + } else { + $c = null; + } + } + $isClass = $c !== null; + } + $className = $isClass ? $c->getName() : null; + + if ($className !== null) { + $dependencies[$param->getName()] = Instance::of($className, $this->isNulledParam($param)); } else { - $c = $param->getClass(); - $dependencies[] = Instance::of($c === null ? null : $c->getName()); + $dependencies[$param->getName()] = $param->isDefaultValueAvailable() + ? $param->getDefaultValue() + : null; } } } @@ -454,6 +566,15 @@ class Container extends Component } /** + * @param ReflectionParameter $param + * @return bool + */ + private function isNulledParam($param) + { + return $param->isOptional() || (PHP_VERSION_ID >= 70100 && $param->getType()->allowsNull()); + } + + /** * Resolves dependencies by replacing them with the actual object instances. * @param array $dependencies the dependencies * @param ReflectionClass $reflection the class reflection associated with the dependencies @@ -465,12 +586,14 @@ class Container extends Component foreach ($dependencies as $index => $dependency) { if ($dependency instanceof Instance) { if ($dependency->id !== null) { - $dependencies[$index] = $this->get($dependency->id); + $dependencies[$index] = $dependency->get($this); } elseif ($reflection !== null) { $name = $reflection->getConstructor()->getParameters()[$index]->getName(); $class = $reflection->getName(); throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\"."); } + } elseif ($this->_resolveArrays && is_array($dependency)) { + $dependencies[$index] = $this->resolveDependencies($dependency, $reflection); } } @@ -537,12 +660,23 @@ class Container extends Component foreach ($reflection->getParameters() as $param) { $name = $param->getName(); - if (($class = $param->getClass()) !== null) { + + if (PHP_VERSION_ID >= 80000) { + $class = $param->getType(); + $isClass = $class !== null && !$param->getType()->isBuiltin(); + } else { + $class = $param->getClass(); + $isClass = $class !== null; + } + + if ($isClass) { $className = $class->getName(); - if (version_compare(PHP_VERSION, '5.6.0', '>=') && $param->isVariadic()) { + if (PHP_VERSION_ID >= 50600 && $param->isVariadic()) { $args = array_merge($args, array_values($params)); break; - } elseif ($associative && isset($params[$name]) && $params[$name] instanceof $className) { + } + + if ($associative && isset($params[$name]) && $params[$name] instanceof $className) { $args[] = $params[$name]; unset($params[$name]); } elseif (!$associative && isset($params[0]) && $params[0] instanceof $className) { @@ -630,7 +764,7 @@ class Container extends Component public function setDefinitions(array $definitions) { foreach ($definitions as $class => $definition) { - if (is_array($definition) && count($definition) === 2 && array_values($definition) === $definition) { + if (is_array($definition) && count($definition) === 2 && array_values($definition) === $definition && is_array($definition[1])) { $this->set($class, $definition[0], $definition[1]); continue; } @@ -660,4 +794,13 @@ class Container extends Component $this->setSingleton($class, $definition); } } + + /** + * @param bool $value whether to attempt to resolve elements in array dependencies + * @since 2.0.37 + */ + public function setResolveArrays($value) + { + $this->_resolveArrays = (bool) $value; + } } diff --git a/framework/di/Instance.php b/framework/di/Instance.php index af0044c..5294386 100644 --- a/framework/di/Instance.php +++ b/framework/di/Instance.php @@ -7,6 +7,7 @@ namespace yii\di; +use Exception; use Yii; use yii\base\InvalidConfigException; @@ -59,25 +60,32 @@ class Instance * @var string the component ID, class name, interface name or alias name */ public $id; + /** + * @var bool if null should be returned instead of throwing an exception + */ + public $optional; /** * Constructor. * @param string $id the component ID + * @param bool $optional if null should be returned instead of throwing an exception */ - protected function __construct($id) + protected function __construct($id, $optional = false) { $this->id = $id; + $this->optional = $optional; } /** * Creates a new Instance object. * @param string $id the component ID + * @param bool $optional if null should be returned instead of throwing an exception * @return Instance the new Instance object. */ - public static function of($id) + public static function of($id, $optional = false) { - return new static($id); + return new static($id, $optional); } /** @@ -157,14 +165,21 @@ class Instance */ public function get($container = null) { - if ($container) { - return $container->get($this->id); - } - if (Yii::$app && Yii::$app->has($this->id)) { - return Yii::$app->get($this->id); - } + try { + if ($container) { + return $container->get($this->id); + } + if (Yii::$app && Yii::$app->has($this->id)) { + return Yii::$app->get($this->id); + } - return Yii::$container->get($this->id); + return Yii::$container->get($this->id); + } catch (Exception $e) { + if ($this->optional) { + return null; + } + throw $e; + } } /** diff --git a/framework/di/ServiceLocator.php b/framework/di/ServiceLocator.php index eeb557c..ab8b6a4 100644 --- a/framework/di/ServiceLocator.php +++ b/framework/di/ServiceLocator.php @@ -199,7 +199,11 @@ class ServiceLocator extends Component $this->_definitions[$id] = $definition; } elseif (is_array($definition)) { // a configuration array - if (isset($definition['class'])) { + if (isset($definition['__class'])) { + $this->_definitions[$id] = $definition; + $this->_definitions[$id]['class'] = $definition['__class']; + unset($this->_definitions[$id]['__class']); + } elseif (isset($definition['class'])) { $this->_definitions[$id] = $definition; } else { throw new InvalidConfigException("The configuration for the \"$id\" component must contain a \"class\" element."); diff --git a/framework/filters/AccessRule.php b/framework/filters/AccessRule.php index 5a52fa8..606c170 100644 --- a/framework/filters/AccessRule.php +++ b/framework/filters/AccessRule.php @@ -114,8 +114,11 @@ class AccessRule extends Component * @var array list of user IP addresses that this rule applies to. An IP address * can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix. * For example, '192.168.*' matches all IP addresses in the segment '192.168.'. + * It may also contain a pattern/mask like '172.16.0.0/12' which would match all IPs from the + * 20-bit private network block in RFC1918. * If not set or empty, it means this rule applies to all IP addresses. * @see Request::userIP + * @see IpHelper::inRange */ public $ips; /** diff --git a/framework/filters/ContentNegotiator.php b/framework/filters/ContentNegotiator.php index 84ca104..760c4a0 100644 --- a/framework/filters/ContentNegotiator.php +++ b/framework/filters/ContentNegotiator.php @@ -11,9 +11,9 @@ use Yii; use yii\base\ActionFilter; use yii\base\BootstrapInterface; use yii\web\BadRequestHttpException; +use yii\web\NotAcceptableHttpException; use yii\web\Request; use yii\web\Response; -use yii\web\UnsupportedMediaTypeHttpException; /** * ContentNegotiator supports response format negotiation and application language negotiation. @@ -87,14 +87,14 @@ class ContentNegotiator extends ActionFilter implements BootstrapInterface { /** * @var string the name of the GET parameter that specifies the response format. - * Note that if the specified format does not exist in [[formats]], a [[UnsupportedMediaTypeHttpException]] + * Note that if the specified format does not exist in [[formats]], a [[NotAcceptableHttpException]] * exception will be thrown. If the parameter value is empty or if this property is null, * the response format will be determined based on the `Accept` HTTP header only. * @see formats */ public $formatParam = '_format'; /** - * @var string the name of the GET parameter that specifies the [[\yii\base\Application::language|application language]]. + * @var string the name of the GET parameter that specifies the [[\yii\base\Application::$language|application language]]. * Note that if the specified language does not match any of [[languages]], the first language in [[languages]] * will be used. If the parameter value is empty or if this property is null, * the application language will be determined based on the `Accept-Language` HTTP header only. @@ -104,7 +104,7 @@ class ContentNegotiator extends ActionFilter implements BootstrapInterface /** * @var array list of supported response formats. The keys are MIME types (e.g. `application/json`) * while the values are the corresponding formats (e.g. `html`, `json`) which must be supported - * as declared in [[\yii\web\Response::formatters]]. + * as declared in [[\yii\web\Response::$formatters]]. * * If this property is empty or not set, response format negotiation will be skipped. */ @@ -172,7 +172,7 @@ class ContentNegotiator extends ActionFilter implements BootstrapInterface * @param Request $request * @param Response $response * @throws BadRequestHttpException if an array received for GET parameter [[formatParam]]. - * @throws UnsupportedMediaTypeHttpException if none of the requested content types is accepted. + * @throws NotAcceptableHttpException if none of the requested content types is accepted. */ protected function negotiateContentType($request, $response) { @@ -188,7 +188,7 @@ class ContentNegotiator extends ActionFilter implements BootstrapInterface return; } - throw new UnsupportedMediaTypeHttpException('The requested response format is not supported: ' . $format); + throw new NotAcceptableHttpException('The requested response format is not supported: ' . $format); } $types = $request->getAcceptableContentTypes(); @@ -216,7 +216,7 @@ class ContentNegotiator extends ActionFilter implements BootstrapInterface return; } - throw new UnsupportedMediaTypeHttpException('None of your requested content types is supported.'); + throw new NotAcceptableHttpException('None of your requested content types is supported.'); } /** diff --git a/framework/filters/PageCache.php b/framework/filters/PageCache.php index 1930aa0..774e8db 100644 --- a/framework/filters/PageCache.php +++ b/framework/filters/PageCache.php @@ -124,6 +124,7 @@ class PageCache extends ActionFilter implements DynamicContentAwareInterface * @var bool|array a boolean value indicating whether to cache all cookies, or an array of * cookie names indicating which cookies can be cached. Be very careful with caching cookies, because * it may leak sensitive or private data stored in cookies to unwanted users. + * @see insertResponseCollectionIntoData() * @since 2.0.4 */ public $cacheCookies = false; @@ -131,6 +132,7 @@ class PageCache extends ActionFilter implements DynamicContentAwareInterface * @var bool|array a boolean value indicating whether to cache all HTTP headers, or an array of * HTTP header names (case-insensitive) indicating which HTTP headers can be cached. * Note if your HTTP headers contain sensitive information, you should white-list which headers can be cached. + * @see insertResponseCollectionIntoData() * @since 2.0.4 */ public $cacheHeaders = true; diff --git a/framework/filters/RateLimiter.php b/framework/filters/RateLimiter.php index dea1d48..daa8310 100644 --- a/framework/filters/RateLimiter.php +++ b/framework/filters/RateLimiter.php @@ -7,6 +7,7 @@ namespace yii\filters; +use Closure; use Yii; use yii\base\ActionFilter; use yii\web\Request; @@ -48,8 +49,14 @@ class RateLimiter extends ActionFilter */ public $errorMessage = 'Rate limit exceeded.'; /** - * @var RateLimitInterface the user object that implements the RateLimitInterface. - * If not set, it will take the value of `Yii::$app->user->getIdentity(false)`. + * @var RateLimitInterface|Closure the user object that implements the RateLimitInterface. If not set, it will take the value of `Yii::$app->user->getIdentity(false)`. + * {@since 2.0.38} It's possible to provide a closure function in order to assign the user identity on runtime. Using a closure to assign the user identity is recommend + * when you are **not** using the standard `Yii::$app->user` component. See the example below: + * ```php + * 'user' => function() { + * return Yii::$app->apiUser->identity; + * } + * ``` */ public $user; /** @@ -84,6 +91,10 @@ class RateLimiter extends ActionFilter $this->user = Yii::$app->getUser()->getIdentity(false); } + if ($this->user instanceof Closure) { + $this->user = call_user_func($this->user, $action); + } + if ($this->user instanceof RateLimitInterface) { Yii::debug('Check rate limit', __METHOD__); $this->checkRateLimit($this->user, $this->request, $this->response, $action); diff --git a/framework/filters/auth/HttpBasicAuth.php b/framework/filters/auth/HttpBasicAuth.php index 7739dc9..3d60077 100644 --- a/framework/filters/auth/HttpBasicAuth.php +++ b/framework/filters/auth/HttpBasicAuth.php @@ -101,7 +101,7 @@ class HttpBasicAuth extends AuthMethod if ($identity === null) { $this->handleFailure($response); } elseif ($user->getIdentity(false) !== $identity) { - $user->switchIdentity($identity); + $user->login($identity); } return $identity; diff --git a/framework/grid/DataColumn.php b/framework/grid/DataColumn.php index be16101..8760104 100644 --- a/framework/grid/DataColumn.php +++ b/framework/grid/DataColumn.php @@ -117,7 +117,24 @@ class DataColumn extends Column * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. */ public $filterInputOptions = ['class' => 'form-control', 'id' => null]; + /** + * @var string the attribute name of the [[GridView::filterModel]] associated with this column. If not set, + * will have the same value as [[attribute]]. + * @since 2.0.41 + */ + public $filterAttribute; + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + if($this->filterAttribute === null) { + $this->filterAttribute = $this->attribute; + } + } /** * {@inheritdoc} @@ -142,7 +159,7 @@ class DataColumn extends Column } /** - * {@inheritdoc] + * {@inheritdoc} * @since 2.0.8 */ protected function getHeaderCellLabel() @@ -161,7 +178,7 @@ class DataColumn extends Column $model = $modelClass::instance(); $label = $model->getAttributeLabel($this->attribute); } elseif ($this->grid->filterModel !== null && $this->grid->filterModel instanceof Model) { - $label = $this->grid->filterModel->getAttributeLabel($this->attribute); + $label = $this->grid->filterModel->getAttributeLabel($this->filterAttribute); } else { $models = $provider->getModels(); if (($model = reset($models)) instanceof Model) { @@ -189,25 +206,26 @@ class DataColumn extends Column $model = $this->grid->filterModel; - if ($this->filter !== false && $model instanceof Model && $this->attribute !== null && $model->isAttributeActive($this->attribute)) { - if ($model->hasErrors($this->attribute)) { + if ($this->filter !== false && $model instanceof Model && $this->filterAttribute !== null && $model->isAttributeActive($this->filterAttribute)) { + if ($model->hasErrors($this->filterAttribute)) { Html::addCssClass($this->filterOptions, 'has-error'); - $error = ' ' . Html::error($model, $this->attribute, $this->grid->filterErrorOptions); + $error = ' ' . Html::error($model, $this->filterAttribute, $this->grid->filterErrorOptions); } else { $error = ''; } if (is_array($this->filter)) { $options = array_merge(['prompt' => ''], $this->filterInputOptions); - return Html::activeDropDownList($model, $this->attribute, $this->filter, $options) . $error; + return Html::activeDropDownList($model, $this->filterAttribute, $this->filter, $options) . $error; } elseif ($this->format === 'boolean') { $options = array_merge(['prompt' => ''], $this->filterInputOptions); - return Html::activeDropDownList($model, $this->attribute, [ + return Html::activeDropDownList($model, $this->filterAttribute, [ 1 => $this->grid->formatter->booleanFormat[1], 0 => $this->grid->formatter->booleanFormat[0], ], $options) . $error; } + $options = array_merge(['maxlength' => true], $this->filterInputOptions); - return Html::activeTextInput($model, $this->attribute, $this->filterInputOptions) . $error; + return Html::activeTextInput($model, $this->filterAttribute, $options) . $error; } return parent::renderFilterCellContent(); diff --git a/framework/helpers/BaseArrayHelper.php b/framework/helpers/BaseArrayHelper.php index c5deb6f..8bc7ef2 100644 --- a/framework/helpers/BaseArrayHelper.php +++ b/framework/helpers/BaseArrayHelper.php @@ -7,6 +7,8 @@ namespace yii\helpers; +use ArrayAccess; +use Traversable; use Yii; use yii\base\Arrayable; use yii\base\InvalidArgumentException; @@ -194,7 +196,11 @@ class BaseArrayHelper $key = $lastKey; } - if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) { + if (is_object($array) && property_exists($array, $key)) { + return $array->$key; + } + + if (static::keyExists($key, $array)) { return $array[$key]; } @@ -203,12 +209,20 @@ class BaseArrayHelper $key = substr($key, $pos + 1); } + if (static::keyExists($key, $array)) { + return $array[$key]; + } if (is_object($array)) { // this is expected to fail if the property does not exist, or __get() is not implemented // it is not reliably possible to check whether a property is accessible beforehand - return $array->$key; - } elseif (is_array($array)) { - return (isset($array[$key]) || array_key_exists($key, $array)) ? $array[$key] : $default; + try { + return $array->$key; + } catch (\Exception $e) { + if ($array instanceof ArrayAccess) { + return $default; + } + throw $e; + } } return $default; @@ -510,7 +524,7 @@ class BaseArrayHelper * ``` * * @param array $array - * @param int|string|\Closure $name + * @param int|string|array|\Closure $name * @param bool $keepKeys whether to maintain the array keys. If false, the resulting array * will be re-indexed with integers. * @return array the list of column values @@ -593,7 +607,7 @@ class BaseArrayHelper * This method enhances the `array_key_exists()` function by supporting case-insensitive * key comparison. * @param string $key the key to check - * @param array $array the array with keys 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 */ @@ -602,7 +616,15 @@ class BaseArrayHelper if ($caseSensitive) { // Function `isset` checks key faster but skips `null`, `array_key_exists` handles this case // https://secure.php.net/manual/en/function.array-key-exists.php#107786 - return isset($array[$key]) || array_key_exists($key, $array); + if (is_array($array) && (isset($array[$key]) || 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()]] + return $array instanceof ArrayAccess && $array->offsetExists($key); + } + + if ($array instanceof ArrayAccess) { + throw new InvalidArgumentException('Second parameter($array) cannot be ArrayAccess in case insensitive mode'); } foreach (array_keys($array) as $k) { @@ -805,12 +827,12 @@ class BaseArrayHelper } /** - * Check whether an array or [[\Traversable]] contains an element. + * Check whether an array or [[Traversable]] contains an element. * * This method does the same as the PHP function [in_array()](https://secure.php.net/manual/en/function.in-array.php) - * but additionally works for objects that implement the [[\Traversable]] interface. + * but additionally works for objects that implement the [[Traversable]] interface. * @param mixed $needle The value to look for. - * @param array|\Traversable $haystack The set of values to search. + * @param array|Traversable $haystack The set of values to search. * @param bool $strict Whether to enable strict (`===`) comparison. * @return bool `true` if `$needle` was found in `$haystack`, `false` otherwise. * @throws InvalidArgumentException if `$haystack` is neither traversable nor an array. @@ -819,7 +841,7 @@ class BaseArrayHelper */ public static function isIn($needle, $haystack, $strict = false) { - if ($haystack instanceof \Traversable) { + if ($haystack instanceof Traversable) { foreach ($haystack as $value) { if ($needle == $value && (!$strict || $needle === $value)) { return true; @@ -835,27 +857,27 @@ class BaseArrayHelper } /** - * Checks whether a variable is an array or [[\Traversable]]. + * Checks whether a variable is an array or [[Traversable]]. * * This method does the same as the PHP function [is_array()](https://secure.php.net/manual/en/function.is-array.php) - * but additionally works on objects that implement the [[\Traversable]] interface. + * but additionally works on objects that implement the [[Traversable]] interface. * @param mixed $var The variable being evaluated. - * @return bool whether $var is array-like + * @return bool whether $var can be traversed via foreach * @see https://secure.php.net/manual/en/function.is-array.php * @since 2.0.8 */ public static function isTraversable($var) { - return is_array($var) || $var instanceof \Traversable; + return is_array($var) || $var instanceof Traversable; } /** - * Checks whether an array or [[\Traversable]] is a subset of another array or [[\Traversable]]. + * Checks whether an array or [[Traversable]] is a subset of another array or [[Traversable]]. * * This method will return `true`, if all elements of `$needles` are contained in * `$haystack`. If at least one element is missing, `false` will be returned. - * @param array|\Traversable $needles The values that must **all** be in `$haystack`. - * @param array|\Traversable $haystack The set of value to search. + * @param array|Traversable $needles The values that must **all** be in `$haystack`. + * @param array|Traversable $haystack The set of value to search. * @param bool $strict Whether to enable strict (`===`) comparison. * @throws InvalidArgumentException if `$haystack` or `$needles` is neither traversable nor an array. * @return bool `true` if `$needles` is a subset of `$haystack`, `false` otherwise. @@ -863,7 +885,7 @@ class BaseArrayHelper */ public static function isSubset($needles, $haystack, $strict = false) { - if (is_array($needles) || $needles instanceof \Traversable) { + if (is_array($needles) || $needles instanceof Traversable) { foreach ($needles as $needle) { if (!static::isIn($needle, $haystack, $strict)) { return false; @@ -923,41 +945,53 @@ class BaseArrayHelper public static function filter($array, $filters) { $result = []; - $forbiddenVars = []; - - foreach ($filters as $var) { - $keys = explode('.', $var); - $globalKey = $keys[0]; - $localKey = isset($keys[1]) ? $keys[1] : null; - - if ($globalKey[0] === '!') { - $forbiddenVars[] = [ - substr($globalKey, 1), - $localKey, - ]; - continue; - } + $excludeFilters = []; - if (!array_key_exists($globalKey, $array)) { + foreach ($filters as $filter) { + if (!is_string($filter) && !is_int($filter)) { continue; } - if ($localKey === null) { - $result[$globalKey] = $array[$globalKey]; + + if (is_string($filter) && strpos($filter, '!') === 0) { + $excludeFilters[] = substr($filter, 1); continue; } - if (!isset($array[$globalKey][$localKey])) { - continue; + + $nodeValue = $array; //set $array as root node + $keys = explode('.', (string) $filter); + foreach ($keys as $key) { + if (!array_key_exists($key, $nodeValue)) { + continue 2; //Jump to next filter + } + $nodeValue = $nodeValue[$key]; } - if (!array_key_exists($globalKey, $result)) { - $result[$globalKey] = []; + + //We've found a value now let's insert it + $resultNode = &$result; + foreach ($keys as $key) { + if (!array_key_exists($key, $resultNode)) { + $resultNode[$key] = []; + } + $resultNode = &$resultNode[$key]; } - $result[$globalKey][$localKey] = $array[$globalKey][$localKey]; + $resultNode = $nodeValue; } - foreach ($forbiddenVars as $var) { - list($globalKey, $localKey) = $var; - if (array_key_exists($globalKey, $result)) { - unset($result[$globalKey][$localKey]); + foreach ($excludeFilters as $filter) { + $excludeNode = &$result; + $keys = explode('.', (string) $filter); + $numNestedKeys = count($keys) - 1; + foreach ($keys as $i => $key) { + if (!array_key_exists($key, $excludeNode)) { + continue 2; //Jump to next filter + } + + if ($i < $numNestedKeys) { + $excludeNode = &$excludeNode[$key]; + } else { + unset($excludeNode[$key]); + break; + } } } diff --git a/framework/helpers/BaseConsole.php b/framework/helpers/BaseConsole.php index f73c82a..2852762 100644 --- a/framework/helpers/BaseConsole.php +++ b/framework/helpers/BaseConsole.php @@ -7,6 +7,7 @@ namespace yii\helpers; +use Yii; use yii\console\Markdown as ConsoleMarkdown; use yii\base\Model; @@ -330,7 +331,7 @@ class BaseConsole */ public static function stripAnsiFormat($string) { - return preg_replace('/\033\[[\d;?]*\w/', '', $string); + return preg_replace(self::ansiCodesPattern(), '', $string); } /** @@ -344,6 +345,78 @@ class BaseConsole } /** + * Returns the width of the string without ANSI color codes. + * @param string $string the string to measure + * @return int the width of the string not counting ANSI format characters + * @since 2.0.36 + */ + public static function ansiStrwidth($string) + { + return mb_strwidth(static::stripAnsiFormat($string), Yii::$app->charset); + } + + /** + * Returns the portion with ANSI color codes of string specified by the start and length parameters. + * If string has color codes, then will be return "TEXT_COLOR + TEXT_STRING + DEFAULT_COLOR", + * else will be simple "TEXT_STRING". + * @param string $string + * @param int $start + * @param int $length + * @return string + */ + public static function ansiColorizedSubstr($string, $start, $length) + { + if ($start < 0 || $length <= 0) { + return ''; + } + + $textItems = preg_split(self::ansiCodesPattern(), $string); + + preg_match_all(self::ansiCodesPattern(), $string, $colors); + $colors = count($colors) ? $colors[0] : []; + array_unshift($colors, ''); + + $result = ''; + $curPos = 0; + $inRange = false; + + foreach ($textItems as $k => $textItem) { + $color = $colors[$k]; + + if ($curPos <= $start && $start < $curPos + Console::ansiStrwidth($textItem)) { + $text = mb_substr($textItem, $start - $curPos, null, Yii::$app->charset); + $inRange = true; + } else { + $text = $textItem; + } + + if ($inRange) { + $result .= $color . $text; + $diff = $length - Console::ansiStrwidth($result); + if ($diff <= 0) { + if ($diff < 0) { + $result = mb_substr($result, 0, $diff, Yii::$app->charset); + } + $defaultColor = static::renderColoredString('%n'); + if ($color && $color != $defaultColor) { + $result .= $defaultColor; + } + break; + } + } + + $curPos += mb_strlen($textItem, Yii::$app->charset); + } + + return $result; + } + + private static function ansiCodesPattern() + { + return /** @lang PhpRegExp */ '/\033\[[\d;?]*\w/'; + } + + /** * Converts an ANSI formatted string to HTML. * * Note: xTerm 256 bit colors are currently not supported. @@ -897,7 +970,7 @@ class BaseConsole /** * Starts display of a progress bar on screen. * - * This bar will be updated by [[updateProgress()]] and my be ended by [[endProgress()]]. + * This bar will be updated by [[updateProgress()]] and may be ended by [[endProgress()]]. * * The following example shows a simple usage of a progress bar: * @@ -911,6 +984,7 @@ class BaseConsole * ``` * * Git clone like progress (showing only status information): + * * ```php * Console::startProgress(0, 1000, 'Counting objects: ', false); * for ($n = 1; $n <= 1000; $n++) { diff --git a/framework/helpers/BaseFileHelper.php b/framework/helpers/BaseFileHelper.php index 828ecbf..71bfe8f 100644 --- a/framework/helpers/BaseFileHelper.php +++ b/framework/helpers/BaseFileHelper.php @@ -700,7 +700,7 @@ class BaseFileHelper private static function matchPathname($path, $basePath, $pattern, $firstWildcard, $flags) { // match with FNM_PATHNAME; the pattern has base implicitly in front of it. - if (isset($pattern[0]) && $pattern[0] === '/') { + if (strpos($pattern, '/') === 0) { $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); if ($firstWildcard !== false && $firstWildcard !== 0) { $firstWildcard--; @@ -806,11 +806,11 @@ class BaseFileHelper $result['flags'] |= self::PATTERN_CASE_INSENSITIVE; } - if (!isset($pattern[0])) { + if (empty($pattern)) { return $result; } - if ($pattern[0] === '!') { + if (strpos($pattern, '!') === 0) { $result['flags'] |= self::PATTERN_NEGATIVE; $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); } @@ -822,7 +822,7 @@ class BaseFileHelper $result['flags'] |= self::PATTERN_NODIR; } $result['firstWildcard'] = self::firstWildcardInPattern($pattern); - if ($pattern[0] === '*' && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) { + if (strpos($pattern, '*') === 0 && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) { $result['flags'] |= self::PATTERN_ENDSWITH; } $result['pattern'] = $pattern; diff --git a/framework/helpers/BaseHtml.php b/framework/helpers/BaseHtml.php index e346ea6..9b6eee2 100644 --- a/framework/helpers/BaseHtml.php +++ b/framework/helpers/BaseHtml.php @@ -93,7 +93,7 @@ class BaseHtml * will be generated instead of one: `data-name="xyz" data-age="13"`. * @since 2.0.3 */ - public static $dataAttributes = ['data', 'data-ng', 'ng']; + public static $dataAttributes = ['aria', 'data', 'data-ng', 'ng']; /** @@ -420,7 +420,7 @@ class BaseHtml * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code * such as an image tag. If this is coming from end users, you should consider [[encode()]] * it to prevent XSS attacks. - * @param string $email email address. If this is null, the first parameter (link body) will be treated + * @param string|null $email email address. If this is null, the first parameter (link body) will be treated * as the email address and used. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. @@ -470,7 +470,7 @@ class BaseHtml * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code * such as an image tag. If this is is coming from end users, you should [[encode()]] * it to prevent XSS attacks. - * @param string $for the ID of the HTML element that this label is associated with. + * @param string|null $for the ID of the HTML element that this label is associated with. * If this is null, the "for" attribute will not be generated. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. @@ -545,8 +545,8 @@ class BaseHtml /** * Generates an input type of the given type. * @param string $type the type attribute. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string|null $name the name attribute. If it is null, the name attribute will not be generated. + * @param string|null $value the value attribute. If it is null, the value attribute will not be generated. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. @@ -617,7 +617,7 @@ class BaseHtml /** * Generates a text input field. * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string|null $value the value attribute. If it is null, the value attribute will not be generated. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. @@ -632,7 +632,7 @@ class BaseHtml /** * Generates a hidden input field. * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string|null $value the value attribute. If it is null, the value attribute will not be generated. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. @@ -647,7 +647,7 @@ class BaseHtml /** * Generates a password input field. * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string|null $value the value attribute. If it is null, the value attribute will not be generated. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. @@ -665,7 +665,7 @@ class BaseHtml * be "multipart/form-data". After the form is submitted, the uploaded file information * can be obtained via $_FILES[$name] (see PHP documentation). * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string|null $value the value attribute. If it is null, the value attribute will not be generated. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. @@ -819,6 +819,8 @@ class BaseHtml * Defaults to false. * - encode: bool, whether to encode option prompt and option value characters. * Defaults to `true`. This option is available since 2.0.3. + * - strict: boolean, if `$selection` is an array and this value is true a strict comparison will be performed on `$items` keys. Defaults to false. + * This option is available since 2.0.37. * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. @@ -877,6 +879,8 @@ class BaseHtml * Defaults to false. * - encode: bool, whether to encode option prompt and option value characters. * Defaults to `true`. This option is available since 2.0.3. + * - strict: boolean, if `$selection` is an array and this value is true a strict comparison will be performed on `$items` keys. Defaults to false. + * This option is available since 2.0.37. * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. @@ -931,6 +935,8 @@ class BaseHtml * This option is available since version 2.0.16. * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. * This option is ignored if `item` option is set. + * - strict: boolean, if `$selection` is an array and this value is true a strict comparison will be performed on `$items` keys. Defaults to false. + * This option is available since 2.0.37. * - separator: string, the HTML code that separates items. * - itemOptions: array, the options for generating the checkbox tag using [[checkbox()]]. * - item: callable, a callback that can be used to customize the generation of the HTML code @@ -954,7 +960,7 @@ class BaseHtml $name .= '[]'; } if (ArrayHelper::isTraversable($selection)) { - $selection = array_map('strval', (array)$selection); + $selection = array_map('strval', ArrayHelper::toArray($selection)); } $formatter = ArrayHelper::remove($options, 'item'); @@ -962,13 +968,14 @@ class BaseHtml $encode = ArrayHelper::remove($options, 'encode', true); $separator = ArrayHelper::remove($options, 'separator', "\n"); $tag = ArrayHelper::remove($options, 'tag', 'div'); + $strict = ArrayHelper::remove($options, 'strict', false); $lines = []; $index = 0; foreach ($items as $value => $label) { $checked = $selection !== null && (!ArrayHelper::isTraversable($selection) && !strcmp($value, $selection) - || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn((string)$value, $selection)); + || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn((string)$value, $selection, $strict)); if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { @@ -1021,6 +1028,8 @@ class BaseHtml * This option is available since version 2.0.16. * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. * This option is ignored if `item` option is set. + * - strict: boolean, if `$selection` is an array and this value is true a strict comparison will be performed on `$items` keys. Defaults to false. + * This option is available since 2.0.37. * - separator: string, the HTML code that separates items. * - itemOptions: array, the options for generating the radio button tag using [[radio()]]. * - item: callable, a callback that can be used to customize the generation of the HTML code @@ -1041,7 +1050,7 @@ class BaseHtml public static function radioList($name, $selection = null, $items = [], $options = []) { if (ArrayHelper::isTraversable($selection)) { - $selection = array_map('strval', (array)$selection); + $selection = array_map('strval', ArrayHelper::toArray($selection)); } $formatter = ArrayHelper::remove($options, 'item'); @@ -1049,6 +1058,7 @@ class BaseHtml $encode = ArrayHelper::remove($options, 'encode', true); $separator = ArrayHelper::remove($options, 'separator', "\n"); $tag = ArrayHelper::remove($options, 'tag', 'div'); + $strict = ArrayHelper::remove($options, 'strict', false); $hidden = ''; if (isset($options['unselect'])) { @@ -1067,7 +1077,7 @@ class BaseHtml foreach ($items as $value => $label) { $checked = $selection !== null && (!ArrayHelper::isTraversable($selection) && !strcmp($value, $selection) - || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn((string)$value, $selection)); + || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn((string)$value, $selection, $strict)); if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { @@ -1822,8 +1832,8 @@ class BaseHtml */ protected static function activeListInput($type, $model, $attribute, $items, $options = []) { - $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); - $selection = isset($options['value']) ? $options['value'] : static::getAttributeValue($model, $attribute); + $name = ArrayHelper::remove($options, 'name', static::getInputName($model, $attribute)); + $selection = ArrayHelper::remove($options, 'value', static::getAttributeValue($model, $attribute)); if (!array_key_exists('unselect', $options)) { $options['unselect'] = ''; } @@ -1854,12 +1864,13 @@ class BaseHtml public static function renderSelectOptions($selection, $items, &$tagOptions = []) { if (ArrayHelper::isTraversable($selection)) { - $selection = array_map('strval', (array)$selection); + $selection = array_map('strval', ArrayHelper::toArray($selection)); } $lines = []; $encodeSpaces = ArrayHelper::remove($tagOptions, 'encodeSpaces', false); $encode = ArrayHelper::remove($tagOptions, 'encode', true); + $strict = ArrayHelper::remove($tagOptions, 'strict', false); if (isset($tagOptions['prompt'])) { $promptOptions = ['value' => '']; if (is_string($tagOptions['prompt'])) { @@ -1887,7 +1898,7 @@ class BaseHtml if (!isset($groupAttrs['label'])) { $groupAttrs['label'] = $key; } - $attrs = ['options' => $options, 'groups' => $groups, 'encodeSpaces' => $encodeSpaces, 'encode' => $encode]; + $attrs = ['options' => $options, 'groups' => $groups, 'encodeSpaces' => $encodeSpaces, 'encode' => $encode, 'strict' => $strict]; $content = static::renderSelectOptions($selection, $value, $attrs); $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); } else { @@ -1896,7 +1907,7 @@ class BaseHtml if (!array_key_exists('selected', $attrs)) { $attrs['selected'] = $selection !== null && (!ArrayHelper::isTraversable($selection) && !strcmp($key, $selection) - || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn((string)$key, $selection)); + || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn((string)$key, $selection, $strict)); } $text = $encode ? static::encode($value) : $value; if ($encodeSpaces) { @@ -1919,17 +1930,19 @@ class BaseHtml * * The values of attributes will be HTML-encoded using [[encode()]]. * - * The "data" attribute is specially handled when it is receiving an array value. In this case, - * the array will be "expanded" and a list data attributes will be rendered. For example, - * if `'data' => ['id' => 1, 'name' => 'yii']`, then this will be rendered: - * `data-id="1" data-name="yii"`. - * Additionally `'data' => ['params' => ['id' => 1, 'name' => 'yii'], 'status' => 'ok']` will be rendered as: - * `data-params='{"id":1,"name":"yii"}' data-status="ok"`. + * `aria` and `data` attributes get special handling when they are set to an array value. In these cases, + * the array will be "expanded" and a list of ARIA/data attributes will be rendered. For example, + * `'aria' => ['role' => 'checkbox', 'value' => 'true']` would be rendered as + * `aria-role="checkbox" aria-value="true"`. + * + * If a nested `data` value is set to an array, it will be JSON-encoded. For example, + * `'data' => ['params' => ['id' => 1, 'name' => 'yii']]` would be rendered as + * `data-params='{"id":1,"name":"yii"}'`. * * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. * @return string the rendering result. If the attributes are not empty, they will be rendered * into a string with a leading white space (so that it can be directly appended to the tag name - * in a tag. If there is no attribute, an empty string will be returned. + * in a tag). If there is no attribute, an empty string will be returned. * @see addCssClass() */ public static function renderTagAttributes($attributes) @@ -1955,7 +1968,11 @@ class BaseHtml foreach ($value as $n => $v) { if (is_array($v)) { $html .= " $name-$n='" . Json::htmlEncode($v) . "'"; - } else { + } elseif (is_bool($v)) { + if ($v) { + $html .= " $name-$n"; + } + } elseif ($v !== null) { $html .= " $name-$n=\"" . static::encode($v) . '"'; } } diff --git a/framework/helpers/BaseInflector.php b/framework/helpers/BaseInflector.php index 6d6f424..410ee59 100644 --- a/framework/helpers/BaseInflector.php +++ b/framework/helpers/BaseInflector.php @@ -477,7 +477,11 @@ class BaseInflector */ public static function slug($string, $replacement = '-', $lowercase = true) { - $parts = explode($replacement, static::transliterate($string)); + if ((string)$replacement !== '') { + $parts = explode($replacement, static::transliterate($string)); + } else { + $parts = [static::transliterate($string)]; + } $replaced = array_map(function ($element) use ($replacement) { $element = preg_replace('/[^a-zA-Z0-9=\s—–-]+/u', '', $element); @@ -485,6 +489,9 @@ class BaseInflector }, $parts); $string = trim(implode($replacement, $replaced), $replacement); + if ((string)$replacement !== '') { + $string = preg_replace('#' . preg_quote($replacement) . '+#', $replacement, $string); + } return $lowercase ? strtolower($string) : $string; } diff --git a/framework/helpers/BaseJson.php b/framework/helpers/BaseJson.php index 7986fd4..0e3663e 100644 --- a/framework/helpers/BaseJson.php +++ b/framework/helpers/BaseJson.php @@ -151,9 +151,17 @@ class BaseJson $expressions['"' . $token . '"'] = $data->expression; return $token; - } elseif ($data instanceof \JsonSerializable) { + } + + if ($data instanceof \JsonSerializable) { return static::processData($data->jsonSerialize(), $expressions, $expPrefix); - } elseif ($data instanceof Arrayable) { + } + + if ($data instanceof \DateTimeInterface) { + return static::processData((array)$data, $expressions, $expPrefix); + } + + if ($data instanceof Arrayable) { $data = $data->toArray(); } elseif ($data instanceof \SimpleXMLElement) { $data = (array) $data; diff --git a/framework/helpers/BaseUrl.php b/framework/helpers/BaseUrl.php index 4c26696..4b44551 100644 --- a/framework/helpers/BaseUrl.php +++ b/framework/helpers/BaseUrl.php @@ -248,7 +248,7 @@ class BaseUrl return $url; } - if (substr($url, 0, 2) === '//') { + if (strpos($url, '//') === 0) { // e.g. //example.com/path/to/resource return $scheme === '' ? $url : "$scheme:$url"; } diff --git a/framework/helpers/ReplaceArrayValue.php b/framework/helpers/ReplaceArrayValue.php index 4539c42..a408af2 100644 --- a/framework/helpers/ReplaceArrayValue.php +++ b/framework/helpers/ReplaceArrayValue.php @@ -85,7 +85,7 @@ class ReplaceArrayValue public static function __set_state($state) { if (!isset($state['value'])) { - throw new InvalidConfigException('Failed to instantiate class "Instance". Required parameter "id" is missing'); + throw new InvalidConfigException('Failed to instantiate class "ReplaceArrayValue". Required parameter "value" is missing'); } return new self($state['value']); diff --git a/framework/helpers/mimeTypes.php b/framework/helpers/mimeTypes.php index 9571307..3056874 100644 --- a/framework/helpers/mimeTypes.php +++ b/framework/helpers/mimeTypes.php @@ -480,7 +480,6 @@ return [ 'mpkg' => 'application/vnd.apple.installer+xml', 'mpm' => 'application/vnd.blueice.multipass', 'mpn' => 'application/vnd.mophun.application', - 0 => 'application/vnd.lotus-1-2-3', 'mpp' => 'application/vnd.ms-project', 'mpt' => 'application/vnd.ms-project', 'mpy' => 'application/vnd.ibm.minipay', @@ -492,6 +491,7 @@ return [ 'mseed' => 'application/vnd.fdsn.mseed', 'mseq' => 'application/vnd.mseq', 'msf' => 'application/vnd.epson.msf', + 0 => 'application/vnd.lotus-1-2-3', 'msh' => 'model/mesh', 'msi' => 'application/x-msdownload', 'msl' => 'application/vnd.mobius.msl', @@ -553,6 +553,7 @@ return [ 'opf' => 'application/oebps-package+xml', 'opml' => 'text/x-opml', 'oprc' => 'application/vnd.palm', + 'opus' => 'audio/ogg', 'org' => 'application/vnd.lotus-organizer', 'osf' => 'application/vnd.yamaha.openscoreformat', 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index 577fb07..acf7102 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -97,6 +97,13 @@ class Formatter extends Component */ public $locale; /** + * @var string the language code (e.g. `en-US`, `en`) that is used to translate internal messages. + * If not set, [[locale]] will be used (without the `@calendar` param, if included). + * + * @since 2.0.28 + */ + public $language; + /** * @var string the time zone to use for formatting time and date values. * * This can be any value that may be passed to [date_default_timezone_set()](https://secure.php.net/manual/en/function.date-default-timezone-set.php) @@ -209,6 +216,13 @@ class Formatter extends Component */ public $decimalSeparator; /** + * @var string the character displayed as the decimal point when formatting a currency. + * If not set, the currency decimal separator corresponding to [[locale]] will be used. + * If [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, setting this property will have no effect. + * @since 2.0.35 + */ + public $currencyDecimalSeparator; + /** * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number. * If not set, the thousand separator corresponding to [[locale]] will be used. * If [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, the default value is ','. @@ -385,11 +399,14 @@ class Formatter extends Component if ($this->locale === null) { $this->locale = Yii::$app->language; } + if ($this->language === null) { + $this->language = strtok($this->locale, '@'); + } if ($this->booleanFormat === null) { - $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)]; + $this->booleanFormat = [Yii::t('yii', 'No', [], $this->language), Yii::t('yii', 'Yes', [], $this->language)]; } if ($this->nullDisplay === null) { - $this->nullDisplay = '' . Yii::t('yii', '(not set)', [], $this->locale) . ''; + $this->nullDisplay = '' . Yii::t('yii', '(not set)', [], $this->language) . ''; } $this->_intlLoaded = extension_loaded('intl'); if (!$this->_intlLoaded) { @@ -597,7 +614,7 @@ class Formatter extends Component /** * Formats the value as a date. - * @param int|string|DateTime $value the value to be formatted. The following + * @param int|string|DateTime|DateTimeInterface $value the value to be formatted. The following * types of value are supported: * * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition. @@ -610,7 +627,7 @@ class Formatter extends Component * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value. * Also no conversion will be performed on values that have no time information, e.g. `"2017-06-05"`. * - * @param string $format the format used to convert the value into a date string. + * @param string|null $format the format used to convert the value into a date string. * If null, [[dateFormat]] will be used. * * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. @@ -635,7 +652,7 @@ class Formatter extends Component /** * Formats the value as a time. - * @param int|string|DateTime $value the value to be formatted. The following + * @param int|string|DateTime|DateTimeInterface $value the value to be formatted. The following * types of value are supported: * * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition. @@ -647,7 +664,7 @@ class Formatter extends Component * The formatter will convert date values according to [[timeZone]] before formatting it. * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value. * - * @param string $format the format used to convert the value into a date string. + * @param string|null $format the format used to convert the value into a date string. * If null, [[timeFormat]] will be used. * * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. @@ -672,7 +689,7 @@ class Formatter extends Component /** * Formats the value as a datetime. - * @param int|string|DateTime $value the value to be formatted. The following + * @param int|string|DateTime|DateTimeInterface $value the value to be formatted. The following * types of value are supported: * * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition. @@ -684,7 +701,7 @@ class Formatter extends Component * The formatter will convert date values according to [[timeZone]] before formatting it. * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value. * - * @param string $format the format used to convert the value into a date string. + * @param string|null $format the format used to convert the value into a date string. * If null, [[datetimeFormat]] will be used. * * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. @@ -718,7 +735,7 @@ class Formatter extends Component ]; /** - * @param int|string|DateTime $value the value to be formatted. The following + * @param int|string|DateTime|DateTimeInterface $value the value to be formatted. The following * types of value are supported: * * - an integer representing a UNIX timestamp @@ -794,7 +811,7 @@ class Formatter extends Component /** * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods. * - * @param int|string|DateTime $value the datetime value to be normalized. The following + * @param int|string|DateTime|DateTimeInterface $value the datetime value to be normalized. The following * types of value are supported: * * - an integer representing a UNIX timestamp @@ -854,7 +871,7 @@ class Formatter extends Component /** * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970). - * @param int|string|DateTime $value the value to be formatted. The following + * @param int|string|DateTime|DateTimeInterface $value the value to be formatted. The following * types of value are supported: * * - an integer representing a UNIX timestamp @@ -882,7 +899,7 @@ class Formatter extends Component * 2. Using a timestamp that is relative to the `$referenceTime`. * 3. Using a `DateInterval` object. * - * @param int|string|DateTime|DateInterval $value the value to be formatted. The following + * @param int|string|DateTime|DateTimeInterface|DateInterval $value the value to be formatted. The following * types of value are supported: * * - an integer representing a UNIX timestamp @@ -891,7 +908,7 @@ class Formatter extends Component * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future) * - * @param int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now` + * @param int|string|DateTime|DateTimeInterface|null $referenceTime if specified the value is used as a reference time instead of `now` * when `$value` is not a `DateInterval` object. * @return string the formatted result. * @throws InvalidArgumentException if the input value can not be evaluated as a date value. @@ -934,47 +951,47 @@ class Formatter extends Component if ($interval->invert) { if ($interval->y >= 1) { - return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale); + return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->language); } if ($interval->m >= 1) { - return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale); + return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->language); } if ($interval->d >= 1) { - return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale); + return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->language); } if ($interval->h >= 1) { - return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale); + return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->language); } if ($interval->i >= 1) { - return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale); + return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->language); } if ($interval->s == 0) { - return Yii::t('yii', 'just now', [], $this->locale); + return Yii::t('yii', 'just now', [], $this->language); } - return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale); + return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->language); } if ($interval->y >= 1) { - return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale); + return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->language); } if ($interval->m >= 1) { - return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale); + return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->language); } if ($interval->d >= 1) { - return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale); + return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->language); } if ($interval->h >= 1) { - return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale); + return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->language); } if ($interval->i >= 1) { - return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale); + return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->language); } if ($interval->s == 0) { - return Yii::t('yii', 'just now', [], $this->locale); + return Yii::t('yii', 'just now', [], $this->language); } - return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale); + return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->language); } /** @@ -1019,25 +1036,25 @@ class Formatter extends Component $parts = []; if ($interval->y > 0) { - $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->locale); + $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->language); } if ($interval->m > 0) { - $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->locale); + $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->language); } if ($interval->d > 0) { - $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->locale); + $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->language); } if ($interval->h > 0) { - $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->locale); + $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->language); } if ($interval->i > 0) { - $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->locale); + $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->language); } if ($interval->s > 0) { - $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale); + $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->language); } if ($interval->s === 0 && empty($parts)) { - $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale); + $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->language); $isNegative = false; } @@ -1097,7 +1114,7 @@ class Formatter extends Component * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong. * * @param mixed $value the value to be formatted. - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * If not given, the number of digits depends in the input value and is determined based on * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured * using [[$numberFormatterOptions]]. @@ -1147,7 +1164,7 @@ class Formatter extends Component * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong. * * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`. - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * If not given, the number of digits depends in the input value and is determined based on * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured * using [[$numberFormatterOptions]]. @@ -1192,7 +1209,7 @@ class Formatter extends Component * Formats the value as a scientific number. * * @param mixed $value the value to be formatted. - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * If not given, the number of digits depends in the input value and is determined based on * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured * using [[$numberFormatterOptions]]. @@ -1238,7 +1255,7 @@ class Formatter extends Component * scientific notation otherwise the output might be wrong. * * @param mixed $value the value to be formatted. - * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use. + * @param string|null $currency the 3-letter ISO 4217 currency code indicating the currency to use. * If null, [[currencyCode]] will be used. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. @@ -1357,7 +1374,7 @@ class Formatter extends Component * are used in the formatting result. * * @param string|int|float $value value in bytes to be formatted. - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return string the formatted result. @@ -1376,32 +1393,32 @@ class Formatter extends Component if ($this->sizeFormatBase == 1024) { switch ($position) { case 0: - return Yii::t('yii', '{nFormatted} B', $params, $this->locale); + return Yii::t('yii', '{nFormatted} B', $params, $this->language); case 1: - return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} KiB', $params, $this->language); case 2: - return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} MiB', $params, $this->language); case 3: - return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} GiB', $params, $this->language); case 4: - return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} TiB', $params, $this->language); default: - return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} PiB', $params, $this->language); } } else { switch ($position) { case 0: - return Yii::t('yii', '{nFormatted} B', $params, $this->locale); + return Yii::t('yii', '{nFormatted} B', $params, $this->language); case 1: - return Yii::t('yii', '{nFormatted} kB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} kB', $params, $this->language); case 2: - return Yii::t('yii', '{nFormatted} MB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} MB', $params, $this->language); case 3: - return Yii::t('yii', '{nFormatted} GB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} GB', $params, $this->language); case 4: - return Yii::t('yii', '{nFormatted} TB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} TB', $params, $this->language); default: - return Yii::t('yii', '{nFormatted} PB', $params, $this->locale); + return Yii::t('yii', '{nFormatted} PB', $params, $this->language); } } } @@ -1413,7 +1430,7 @@ class Formatter extends Component * are used in the formatting result. * * @param string|int|float $value value in bytes to be formatted. - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return string the formatted result. @@ -1432,32 +1449,32 @@ class Formatter extends Component if ($this->sizeFormatBase == 1024) { switch ($position) { case 0: - return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->language); case 1: - return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->language); case 2: - return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->language); case 3: - return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->language); case 4: - return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->language); default: - return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->language); } } else { switch ($position) { case 0: - return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->language); case 1: - return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->language); case 2: - return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->language); case 3: - return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->language); case 4: - return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->language); default: - return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale); + return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->language); } } } @@ -1468,7 +1485,7 @@ class Formatter extends Component * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]]. * * @param float|int $value value to be formatted. - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * @param array $numberOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return string the formatted result. @@ -1491,7 +1508,7 @@ class Formatter extends Component * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]]. * * @param float|int $value value to be formatted. - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return string the formatted result. @@ -1512,7 +1529,7 @@ class Formatter extends Component * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]]. * * @param float|int $value value to be formatted. - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return string the formatted result. @@ -1534,7 +1551,7 @@ class Formatter extends Component * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]]. * * @param float|int $value value to be formatted. - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return string the formatted result. @@ -1646,8 +1663,9 @@ class Formatter extends Component * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return array [parameters for Yii::t containing formatted number, internal position of size unit] * @throws InvalidArgumentException if the input value is not numeric or the formatting failed. + * @since 2.0.32 */ - private function formatNumber($value, $decimals, $maxPosition, $formatBase, $options, $textOptions) + protected function formatNumber($value, $decimals, $maxPosition, $formatBase, $options, $textOptions) { $value = $this->normalizeNumericValue($value); @@ -1738,7 +1756,7 @@ class Formatter extends Component * @param int $style the type of the number formatter. * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE - * @param int $decimals the number of digits after the decimal point. + * @param int|null $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return NumberFormatter the created formatter instance @@ -1771,6 +1789,9 @@ class Formatter extends Component if ($this->decimalSeparator !== null) { $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator); } + if ($this->currencyDecimalSeparator !== null) { + $formatter->setSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL, $this->currencyDecimalSeparator); + } if ($this->thousandSeparator !== null) { $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator); $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator); @@ -2002,7 +2023,7 @@ class Formatter extends Component * to the defined decimal digits. * * @param string|int|float $value the value to be formatted. - * @param int $decimals the number of digits after the decimal point. The default value is `0`. + * @param int|null $decimals the number of digits after the decimal point. The default value is `0`. * @return string the formatted result. * @since 2.0.16 */ @@ -2042,7 +2063,7 @@ class Formatter extends Component * Fallback for formatting value as a currency number. * * @param string|int|float $value the value to be formatted. - * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use. + * @param string|null $currency the 3-letter ISO 4217 currency code indicating the currency to use. * If null, [[currencyCode]] will be used. * @return string the formatted result. * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined. diff --git a/framework/i18n/GettextMessageSource.php b/framework/i18n/GettextMessageSource.php index 32a3bf1..251bd7f 100644 --- a/framework/i18n/GettextMessageSource.php +++ b/framework/i18n/GettextMessageSource.php @@ -72,14 +72,12 @@ class GettextMessageSource extends MessageSource $fallbackLanguage = substr($language, 0, 2); $fallbackSourceLanguage = substr($this->sourceLanguage, 0, 2); - if ($fallbackLanguage !== $language) { + if ($fallbackLanguage !== '' && $fallbackLanguage !== $language) { $messages = $this->loadFallbackMessages($category, $fallbackLanguage, $messages, $messageFile); - } elseif ($language === $fallbackSourceLanguage) { + } elseif ($fallbackSourceLanguage !== '' && $language === $fallbackSourceLanguage) { $messages = $this->loadFallbackMessages($category, $this->sourceLanguage, $messages, $messageFile); - } else { - if ($messages === null) { - Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); - } + } elseif ($messages === null) { + Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); } return (array) $messages; @@ -106,7 +104,7 @@ class GettextMessageSource extends MessageSource if ( $messages === null && $fallbackMessages === null && $fallbackLanguage !== $this->sourceLanguage - && $fallbackLanguage !== substr($this->sourceLanguage, 0, 2) + && strpos($this->sourceLanguage, $fallbackLanguage) !== 0 ) { Yii::error("The message file for category '$category' does not exist: $originalMessageFile " . "Fallback file does not exist as well: $fallbackMessageFile", __METHOD__); diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index 49cf93b..3e5bd42 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -19,7 +19,7 @@ use yii\base\InvalidConfigException; * * @property MessageFormatter $messageFormatter The message formatter to be used to format message via ICU * message format. Note that the type of this property differs in getter and setter. See - * [[getMessageFormatter()]] and [[setMessageFormatter()]] for details. + * [[getMessageFormatter()]] and [[setMessageFormatter()]] for details. * * @author Qiang Xue * @since 2.0 diff --git a/framework/i18n/Locale.php b/framework/i18n/Locale.php index 6567c08..86b78de 100644 --- a/framework/i18n/Locale.php +++ b/framework/i18n/Locale.php @@ -16,7 +16,7 @@ use yii\base\InvalidConfigException; * * The class requires [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) to be installed. * - * @property string $currencySymbol This property is read-only. + * @property-read string $currencySymbol This property is read-only. * * @since 2.0.14 */ diff --git a/framework/i18n/MessageFormatter.php b/framework/i18n/MessageFormatter.php index 2b11c03..7aec482 100644 --- a/framework/i18n/MessageFormatter.php +++ b/framework/i18n/MessageFormatter.php @@ -36,8 +36,8 @@ use yii\base\NotSupportedException; * Also messages that are working with the fallback implementation are not necessarily compatible with the * PHP intl MessageFormatter so do not rely on the fallback if you are able to install intl extension somehow. * - * @property string $errorCode Code of the last error. This property is read-only. - * @property string $errorMessage Description of the last error. This property is read-only. + * @property-read string $errorCode Code of the last error. This property is read-only. + * @property-read string $errorMessage Description of the last error. This property is read-only. * * @author Alexander Makarov * @author Carsten Brandt @@ -423,7 +423,7 @@ class MessageFormatter extends Component $selector = trim(mb_substr($selector, $pos + 1, mb_strlen($selector, $charset), $charset)); } if ($message === false && $selector === 'other' || - $selector[0] === '=' && (int) mb_substr($selector, 1, mb_strlen($selector, $charset), $charset) === $arg || + strpos($selector, '=') === 0 && (int) mb_substr($selector, 1, mb_strlen($selector, $charset), $charset) === $arg || $selector === 'one' && $arg - $offset == 1 ) { $message = implode(',', str_replace('#', $arg - $offset, $plural[$i])); diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index ec4dcd1..9758468 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -74,14 +74,12 @@ class PhpMessageSource extends MessageSource $fallbackLanguage = substr($language, 0, 2); $fallbackSourceLanguage = substr($this->sourceLanguage, 0, 2); - if ($language !== $fallbackLanguage) { + if ($fallbackLanguage !== '' && $language !== $fallbackLanguage) { $messages = $this->loadFallbackMessages($category, $fallbackLanguage, $messages, $messageFile); - } elseif ($language === $fallbackSourceLanguage) { + } elseif ($fallbackSourceLanguage !== '' && $language === $fallbackSourceLanguage) { $messages = $this->loadFallbackMessages($category, $this->sourceLanguage, $messages, $messageFile); - } else { - if ($messages === null) { - Yii::warning("The message file for category '$category' does not exist: $messageFile", __METHOD__); - } + } elseif ($messages === null) { + Yii::warning("The message file for category '$category' does not exist: $messageFile", __METHOD__); } return (array) $messages; @@ -108,7 +106,7 @@ class PhpMessageSource extends MessageSource if ( $messages === null && $fallbackMessages === null && $fallbackLanguage !== $this->sourceLanguage - && $fallbackLanguage !== substr($this->sourceLanguage, 0, 2) + && strpos($this->sourceLanguage, $fallbackLanguage) !== 0 ) { Yii::error("The message file for category '$category' does not exist: $originalMessageFile " . "Fallback file does not exist as well: $fallbackMessageFile", __METHOD__); diff --git a/framework/i18n/migrations/schema-mssql.sql b/framework/i18n/migrations/schema-mssql.sql index 479c058..261f807 100644 --- a/framework/i18n/migrations/schema-mssql.sql +++ b/framework/i18n/migrations/schema-mssql.sql @@ -29,7 +29,7 @@ CREATE TABLE [message] ); ALTER TABLE [message] ADD CONSTRAINT [pk_message_id_language] PRIMARY KEY ([id], [language]); -ALTER TABLE [message] ADD CONSTRAINT [fk_message_source_message] FOREIGN KEY ([id]) REFERENCES [source_message] ([id]) ON UPDATE CASCADE ON DELETE NO ACTION; +ALTER TABLE [message] ADD CONSTRAINT [fk_message_source_message] FOREIGN KEY ([id]) REFERENCES [source_message] ([id]) ON UPDATE NO ACTION ON DELETE CASCADE; CREATE INDEX [idx_message_language] on [message] ([language]); -CREATE INDEX [idx_source_message_category] on [source_message] ([category]); \ No newline at end of file +CREATE INDEX [idx_source_message_category] on [source_message] ([category]); diff --git a/framework/i18n/migrations/schema-mysql.sql b/framework/i18n/migrations/schema-mysql.sql index dbbbd93..dec180c 100644 --- a/framework/i18n/migrations/schema-mysql.sql +++ b/framework/i18n/migrations/schema-mysql.sql @@ -27,7 +27,7 @@ CREATE TABLE `message` ); ALTER TABLE `message` ADD CONSTRAINT `pk_message_id_language` PRIMARY KEY (`id`, `language`); -ALTER TABLE `message` ADD CONSTRAINT `fk_message_source_message` FOREIGN KEY (`id`) REFERENCES `source_message` (`id`) ON UPDATE CASCADE ON DELETE RESTRICT; +ALTER TABLE `message` ADD CONSTRAINT `fk_message_source_message` FOREIGN KEY (`id`) REFERENCES `source_message` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; CREATE INDEX idx_message_language ON message (language); CREATE INDEX idx_source_message_category ON source_message (category); diff --git a/framework/i18n/migrations/schema-pgsql.sql b/framework/i18n/migrations/schema-pgsql.sql index 651ee7b..4e447d3 100644 --- a/framework/i18n/migrations/schema-pgsql.sql +++ b/framework/i18n/migrations/schema-pgsql.sql @@ -29,10 +29,10 @@ CREATE TABLE "message" ); ALTER TABLE "message" ADD CONSTRAINT "pk_message_id_language" PRIMARY KEY ("id", "language"); -ALTER TABLE "message" ADD CONSTRAINT "fk_message_source_message" FOREIGN KEY ("id") REFERENCES "source_message" ("id") ON UPDATE CASCADE ON DELETE RESTRICT; +ALTER TABLE "message" ADD CONSTRAINT "fk_message_source_message" FOREIGN KEY ("id") REFERENCES "source_message" ("id") ON UPDATE RESTRICT ON DELETE CASCADE; CREATE INDEX "idx_message_language" ON "message" USING btree (language); ALTER TABLE "message" CLUSTER ON "idx_message_language"; CREATE INDEX "idx_source_message_category" ON "source_message" USING btree (category); -ALTER TABLE "source_message" CLUSTER ON "idx_source_message_category"; \ No newline at end of file +ALTER TABLE "source_message" CLUSTER ON "idx_source_message_category"; diff --git a/framework/i18n/migrations/schema-sqlite.sql b/framework/i18n/migrations/schema-sqlite.sql index 338bf62..24db67a 100644 --- a/framework/i18n/migrations/schema-sqlite.sql +++ b/framework/i18n/migrations/schema-sqlite.sql @@ -20,11 +20,11 @@ CREATE TABLE `source_message` CREATE TABLE `message` ( - `id` integer NOT NULL REFERENCES `source_message` (`id`) ON UPDATE CASCADE ON DELETE NO ACTION, + `id` integer NOT NULL REFERENCES `source_message` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, `language` varchar(16) NOT NULL, `translation` text, PRIMARY KEY (`id`, `language`) ); CREATE INDEX idx_message_language ON message (language); -CREATE INDEX idx_source_message_category ON source_message (category); \ No newline at end of file +CREATE INDEX idx_source_message_category ON source_message (category); diff --git a/framework/log/Dispatcher.php b/framework/log/Dispatcher.php index 018108c..d888d89 100644 --- a/framework/log/Dispatcher.php +++ b/framework/log/Dispatcher.php @@ -53,7 +53,7 @@ use yii\base\ErrorHandler; * @property int $flushInterval How many messages should be logged before they are sent to targets. This * method returns the value of [[Logger::flushInterval]]. * @property Logger $logger The logger. If not set, [[\Yii::getLogger()]] will be used. Note that the type of - * this property differs in getter and setter. See [[getLogger()]] and [[setLogger()]] for details. + * this property differs in getter and setter. See [[getLogger()]] and [[setLogger()]] for details. * @property int $traceLevel How many application call stacks should be logged together with each message. * This method returns the value of [[Logger::traceLevel]]. Defaults to 0. * @@ -184,19 +184,17 @@ class Dispatcher extends Component { $targetErrors = []; foreach ($this->targets as $target) { - if ($target->enabled) { - try { - $target->collect($messages, $final); - } catch (\Exception $e) { - $target->enabled = false; - $targetErrors[] = [ - 'Unable to send log via ' . get_class($target) . ': ' . ErrorHandler::convertExceptionToVerboseString($e), - Logger::LEVEL_WARNING, - __METHOD__, - microtime(true), - [], - ]; - } + if (!$target->enabled) { + continue; + } + try { + $target->collect($messages, $final); + } catch (\Throwable $t) { + $target->enabled = false; + $targetErrors[] = $this->generateTargetFailErrorMessage($target, $t, __METHOD__); + } catch (\Exception $e) { + $target->enabled = false; + $targetErrors[] = $this->generateTargetFailErrorMessage($target, $e, __METHOD__); } } @@ -204,4 +202,24 @@ class Dispatcher extends Component $this->dispatch($targetErrors, true); } } + + /** + * Generate target error message + * + * @param Target $target log target object + * @param \Throwable|\Exception $throwable catched exception + * @param string $method full method path + * @return array generated error message data + * @since 2.0.32 + */ + protected function generateTargetFailErrorMessage($target, $throwable, $method) + { + return [ + 'Unable to send log via ' . get_class($target) . ': ' . ErrorHandler::convertExceptionToVerboseString($throwable), + Logger::LEVEL_WARNING, + $method, + microtime(true), + [], + ]; + } } diff --git a/framework/log/FileTarget.php b/framework/log/FileTarget.php index d0b598d..a29c673 100644 --- a/framework/log/FileTarget.php +++ b/framework/log/FileTarget.php @@ -101,8 +101,10 @@ class FileTarget extends Target */ public function export() { - $logPath = dirname($this->logFile); - FileHelper::createDirectory($logPath, $this->dirMode, true); + 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) { @@ -121,21 +123,21 @@ class FileTarget extends Target $writeResult = @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); if ($writeResult === false) { $error = error_get_last(); - throw new LogRuntimeException("Unable to export log through file!: {$error['message']}"); + throw new LogRuntimeException("Unable to export log through file ({$this->logFile})!: {$error['message']}"); } $textSize = strlen($text); if ($writeResult < $textSize) { - throw new LogRuntimeException("Unable to export whole log through file! Wrote $writeResult out of $textSize bytes."); + throw new LogRuntimeException("Unable to export whole log through file ({$this->logFile})! Wrote $writeResult out of $textSize bytes."); } } else { $writeResult = @fwrite($fp, $text); if ($writeResult === false) { $error = error_get_last(); - throw new LogRuntimeException("Unable to export log through file!: {$error['message']}"); + throw new LogRuntimeException("Unable to export log through file ({$this->logFile})!: {$error['message']}"); } $textSize = strlen($text); if ($writeResult < $textSize) { - throw new LogRuntimeException("Unable to export whole log through file! Wrote $writeResult out of $textSize bytes."); + throw new LogRuntimeException("Unable to export whole log through file ({$this->logFile})! Wrote $writeResult out of $textSize bytes."); } @flock($fp, LOCK_UN); @fclose($fp); diff --git a/framework/log/Logger.php b/framework/log/Logger.php index 9bbfa4e..76e993d 100644 --- a/framework/log/Logger.php +++ b/framework/log/Logger.php @@ -30,13 +30,13 @@ use yii\base\Component; * to send logged messages to different log targets, such as [[FileTarget|file]], [[EmailTarget|email]], * or [[DbTarget|database]], with the help of the [[dispatcher]]. * - * @property array $dbProfiling The first element indicates the number of SQL statements executed, and the - * second element the total time spent in SQL execution. This property is read-only. - * @property float $elapsedTime The total elapsed time in seconds for current request. This property is + * @property-read array $dbProfiling The first element indicates the number of SQL statements executed, and + * the second element the total time spent in SQL execution. This property is read-only. + * @property-read float $elapsedTime The total elapsed time in seconds for current request. This property is * read-only. - * @property array $profiling The profiling results. Each element is an array consisting of these elements: - * `info`, `category`, `timestamp`, `trace`, `level`, `duration`, `memory`, `memoryDiff`. The `memory` and - * `memoryDiff` values are available since version 2.0.11. This property is read-only. + * @property-read array $profiling The profiling results. Each element is an array consisting of these + * elements: `info`, `category`, `timestamp`, `trace`, `level`, `duration`, `memory`, `memoryDiff`. The `memory` + * and `memoryDiff` values are available since version 2.0.11. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -111,6 +111,11 @@ class Logger extends Component * @var Dispatcher the message dispatcher */ public $dispatcher; + /** + * @var array of event names used to get statistical results of DB queries. + * @since 2.0.41 + */ + public $dbEventNames = ['yii\db\Command::query', 'yii\db\Command::execute']; /** @@ -252,7 +257,7 @@ class Logger extends Component */ public function getDbProfiling() { - $timings = $this->getProfiling(['yii\db\Command::query', 'yii\db\Command::execute']); + $timings = $this->getProfiling($this->dbEventNames); $count = count($timings); $time = 0; foreach ($timings as $timing) { diff --git a/framework/log/Target.php b/framework/log/Target.php index 2a192e8..846bc91 100644 --- a/framework/log/Target.php +++ b/framework/log/Target.php @@ -27,10 +27,10 @@ use yii\web\Request; * may specify [[except]] to exclude messages of certain categories. * * @property bool $enabled Indicates whether this log target is enabled. Defaults to true. Note that the type - * of this property differs in getter and setter. See [[getEnabled()]] and [[setEnabled()]] for details. + * of this property differs in getter and setter. See [[getEnabled()]] and [[setEnabled()]] for details. * @property int $levels The message levels that this target is interested in. This is a bitmap of level * values. Defaults to 0, meaning all available levels. Note that the type of this property differs in getter and - * setter. See [[getLevels()]] and [[setLevels()]] for details. + * setter. See [[getLevels()]] and [[setLevels()]] for details. * * For more details and usage information on Target, see the [guide article on logging & targets](guide:runtime-logging). * diff --git a/framework/mail/BaseMailer.php b/framework/mail/BaseMailer.php index c4546fb..633974d 100644 --- a/framework/mail/BaseMailer.php +++ b/framework/mail/BaseMailer.php @@ -23,7 +23,7 @@ use yii\web\View; * For more details and usage information on BaseMailer, see the [guide article on mailing](guide:tutorial-mailing). * * @property View $view View instance. Note that the type of this property differs in getter and setter. See - * [[getView()]] and [[setView()]] for details. + * [[getView()]] and [[setView()]] for details. * @property string $viewPath The directory that contains the view files for composing mail messages Defaults * to '@app/mail'. * diff --git a/framework/messages/af/yii.php b/framework/messages/af/yii.php index f072740..557e952 100644 --- a/framework/messages/af/yii.php +++ b/framework/messages/af/yii.php @@ -123,7 +123,7 @@ return [ '{nFormatted} B' => '{nFormatted} B', '{nFormatted} GB' => '{nFormatted} GB', '{nFormatted} GiB' => '{nFormatted} GiB', - '{nFormatted} KB' => '{nFormatted} KB', + '{nFormatted} kB' => '{nFormatted} KB', '{nFormatted} KiB' => '{nFormatted} KiB', '{nFormatted} MB' => '{nFormatted} MB', '{nFormatted} MiB' => '{nFormatted} MiB', diff --git a/framework/messages/ar/yii.php b/framework/messages/ar/yii.php index 7fb0124..b5144bd 100644 --- a/framework/messages/ar/yii.php +++ b/framework/messages/ar/yii.php @@ -30,7 +30,7 @@ return [ 'File upload failed.' => '.فشل في تحميل الملف', 'Home' => 'الرئيسية', 'Invalid data received for parameter "{param}".' => 'بيانات غير صالحة قد وردت في "{param}".', - 'Login Required' => 'تسجبل الدخول مطلوب', + 'Login Required' => 'تسجيل الدخول مطلوب', 'Missing required arguments: {params}' => 'البيانات المطلوبة ضرورية: {params}', 'Missing required parameters: {params}' => 'البيانات المطلوبة ضرورية: {params}', 'No' => 'لا', diff --git a/framework/messages/be/yii.php b/framework/messages/be/yii.php index bba2a4f..4d17ee7 100644 --- a/framework/messages/be/yii.php +++ b/framework/messages/be/yii.php @@ -123,7 +123,7 @@ return [ '{nFormatted} B' => '{nFormatted} Б', '{nFormatted} GB' => '{nFormatted} ГБ', '{nFormatted} GiB' => '{nFormatted} ГіБ', - '{nFormatted} KB' => '{nFormatted} КБ', + '{nFormatted} kB' => '{nFormatted} КБ', '{nFormatted} KiB' => '{nFormatted} КіБ', '{nFormatted} MB' => '{nFormatted} МБ', '{nFormatted} MiB' => '{nFormatted} МіБ', diff --git a/framework/messages/bg/yii.php b/framework/messages/bg/yii.php index cdada6b..bb73817 100644 --- a/framework/messages/bg/yii.php +++ b/framework/messages/bg/yii.php @@ -119,11 +119,11 @@ return [ '{delta, plural, =1{a month} other{# months}} ago' => 'преди {delta, plural, =1{месец} other{# месеца}}', '{delta, plural, =1{a second} other{# seconds}} ago' => 'преди {delta, plural, =1{секунда} other{# секунди}}', '{delta, plural, =1{a year} other{# years}} ago' => 'преди {delta, plural, =1{година} other{# години}}', - '{delta, plural, =1{an hour} other{# hours}} ago' => '{delta, plural, =1{час} other{# часа}}', + '{delta, plural, =1{an hour} other{# hours}} ago' => 'преди {delta, plural, =1{час} other{# часа}}', '{nFormatted} B' => '{nFormatted} B', '{nFormatted} GB' => '{nFormatted} GB', '{nFormatted} GiB' => '{nFormatted} GiB', - '{nFormatted} KB' => '{nFormatted} KB', + '{nFormatted} kB' => '{nFormatted} KB', '{nFormatted} KiB' => '{nFormatted} KiB', '{nFormatted} MB' => '{nFormatted} MB', '{nFormatted} MiB' => '{nFormatted} MiB', diff --git a/framework/messages/config.php b/framework/messages/config.php index 00d7d88..7f8db2d 100644 --- a/framework/messages/config.php +++ b/framework/messages/config.php @@ -12,7 +12,7 @@ return [ 'messagePath' => __DIR__, // array, required, list of language codes that the extracted messages // should be translated to. For example, ['zh-CN', 'de']. - 'languages' => ['af', 'ar', 'az', 'be', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'el', 'es', 'et', 'fa', 'fi', 'fr', 'he', 'hr', 'hu', 'hy', 'id', 'it', 'ja', 'ka', 'kk', 'ko', 'kz', 'lt', 'lv', 'ms', 'nb-NO', 'nl', 'pl', 'pt', 'pt-BR', 'ro', 'ru', 'sk', 'sl', 'sr', 'sr-Latn', 'sv', 'tg', 'th', 'tr', 'uk', 'uz', 'vi', 'zh-CN', 'zh-TW'], + 'languages' => ['af', 'ar', 'az', 'be', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'el', 'es', 'et', 'fa', 'fi', 'fr', 'he', 'hi', 'hr', 'hu', 'hy', 'id', 'it', 'ja', 'ka', 'kk', 'ko', 'kz', 'lt', 'lv', 'ms', 'nb-NO', 'nl', 'pl', 'pt', 'pt-BR', 'ro', 'ru', 'sk', 'sl', 'sr', 'sr-Latn', 'sv', 'tg', 'th', 'tr', 'uk', 'uz', 'vi', 'zh-CN', 'zh-TW'], // string, the name of the function for translating messages. // Defaults to 'Yii::t'. This is used as a mark to find the messages to be // translated. You may use a string for single function name or an array for diff --git a/framework/messages/de/yii.php b/framework/messages/de/yii.php index 089e679..823f853 100644 --- a/framework/messages/de/yii.php +++ b/framework/messages/de/yii.php @@ -29,7 +29,7 @@ return [ 'Are you sure you want to delete this item?' => 'Wollen Sie diesen Eintrag wirklich löschen?', 'Delete' => 'Löschen', 'Error' => 'Fehler', - 'File upload failed.' => 'Das Hochladen der Datei ist gescheitert.', + 'File upload failed.' => 'Das Hochladen der Datei ist fehlgeschlagen.', 'Home' => 'Home', 'Invalid data received for parameter "{param}".' => 'Ungültige Daten erhalten für Parameter "{param}".', 'Login Required' => 'Anmeldung erforderlich', @@ -56,15 +56,15 @@ return [ 'The requested view "{name}" was not found.' => 'Die View-Datei "{name}" konnte nicht gefunden werden.', 'The verification code is incorrect.' => 'Der Prüfcode ist falsch.', 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Insgesamt {count, number} {count, plural, one{Eintrag} other{Einträge}}.', - 'Unable to verify your data submission.' => 'Es ist nicht möglich, Ihre Dateneingabe zu prüfen.', + 'Unable to verify your data submission.' => 'Ihre Dateneingabe konnte nicht überprüft werden oder ist ungültig.', 'Unknown alias: -{name}' => 'Unbekannter Alias: -{name}', 'Unknown option: --{name}' => 'Unbekannte Option: --{name}', 'Update' => 'Bearbeiten', 'View' => 'Anzeigen', 'Yes' => 'Ja', 'Yii Framework' => 'Yii Framework', - 'You are not allowed to perform this action.' => 'Sie dürfen diese Aktion nicht durchführen.', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Sie können maximal {limit, plural, one{eine Datei} other{# Dateien}} hochladen.', + 'You are not allowed to perform this action.' => 'Sie dürfen diese Aktion nicht durchführen.', 'in {delta, plural, =1{a day} other{# days}}' => 'in {delta, plural, =1{einem Tag} other{# Tagen}}', 'in {delta, plural, =1{a minute} other{# minutes}}' => 'in {delta, plural, =1{einer Minute} other{# Minuten}}', 'in {delta, plural, =1{a month} other{# months}}' => 'in {delta, plural, =1{einem Monat} other{# Monaten}}', @@ -136,7 +136,7 @@ return [ '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} TebiByte', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} Terabyte', '"{attribute}" does not support operator "{operator}".' => '"{attribute}" unterstützt den Operator "{operator}" nicht.', - 'Condition for "{attribute}" should be either a value or valid operator specification.' => 'Die Bedingung für "{attribute}" muss entweder ein Wert oder ein Operatorvergleich sein.', + 'Condition for "{attribute}" should be either a value or valid operator specification.' => 'Die Bedingung für "{attribute}" muss entweder ein Wert oder ein gültiger Operator sein.', 'Operator "{operator}" must be used with a search attribute.' => 'Der Operator "{operator}" muss zusammen mit einem Such-Attribut verwendet werden.', 'Operator "{operator}" requires multiple operands.' => 'Der Operator "{operator}" erwartet mehrere Operanden.', 'The format of {filter} is invalid.' => 'Das Format von {filter} ist ungültig.', diff --git a/framework/messages/el/yii.php b/framework/messages/el/yii.php index 3d057ed..ed57951 100644 --- a/framework/messages/el/yii.php +++ b/framework/messages/el/yii.php @@ -23,7 +23,6 @@ * NOTE: this file must be saved in UTF-8 encoding. */ return [ - 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => 'Θα πρέπει να ανεβάσετε τουλάχιστον {limit, number} {limit, plural, one{file} other{files}}.', ' and ' => ' και ', '"{attribute}" does not support operator "{operator}".' => 'Το "{attribute}" δεν υποστηρίζει τον τελεστή "{operator}".', '(not set)' => '(μη ορισμένο)', @@ -72,6 +71,7 @@ return [ 'Yii Framework' => 'Yii Framework', 'You are not allowed to perform this action.' => 'Δεν επιτρέπεται να εκτελέσετε αυτή την ενέργεια.', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Μπορείτε να ανεβάσετε το πολύ {limit, number} {limit, plural, one{αρχείο} few{αρχεία} many{αρχεία} other{αρχεία}}.', + 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => 'Θα πρέπει να ανεβάσετε τουλάχιστον {limit, number} {limit, plural, one{αρχείο} other{αρχεία}}.', 'in {delta, plural, =1{a day} other{# days}}' => 'σε {delta, plural, =1{μία ημέρα} other{# ημέρες}}', 'in {delta, plural, =1{a minute} other{# minutes}}' => 'σε {delta, plural, =1{ένα λεπτό} other{# λεπτά}}', 'in {delta, plural, =1{a month} other{# months}}' => 'σε {delta, plural, =1{ένα μήνα} other{# μήνες}}', @@ -123,7 +123,6 @@ return [ '{nFormatted} B' => '{nFormatted} B', '{nFormatted} GB' => '{nFormatted} GB', '{nFormatted} GiB' => '{nFormatted} GiB', - '{nFormatted} kB' => '{nFormatted} kB', '{nFormatted} KiB' => '{nFormatted} KiB', '{nFormatted} MB' => '{nFormatted} MB', '{nFormatted} MiB' => '{nFormatted} MiB', @@ -131,6 +130,7 @@ return [ '{nFormatted} PiB' => '{nFormatted} PiB', '{nFormatted} TB' => '{nFormatted} TB', '{nFormatted} TiB' => '{nFormatted} TiB', + '{nFormatted} kB' => '{nFormatted} kB', '{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} {n, plural, =1{byte} other{bytes}}', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', @@ -142,4 +142,7 @@ return [ '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', + 'Action not found.' => 'Δε βρέθηκε η ενέργεια.', + 'Aliases available: {aliases}' => 'Διαθέσιμα ψευδώνυμα: {aliases}', + 'Options available: {options}' => 'Διαθέσιμες επιλογές: {options}', ]; diff --git a/framework/messages/he/yii.php b/framework/messages/he/yii.php index 3c6c7ea..7e49173 100644 --- a/framework/messages/he/yii.php +++ b/framework/messages/he/yii.php @@ -25,6 +25,7 @@ return [ '(not set)' => '(לא הוגדר)', 'An internal server error occurred.' => 'שגיאת שרת פנימית', + 'Are you sure you want to delete this item?' => 'האם אתה בטוח שברצונך למחוק פריט זה?', 'Delete' => 'מחק', 'Error' => 'שגיאה', 'File upload failed.' => 'העלאת קובץ נכשלה', diff --git a/framework/messages/hi/yii.php b/framework/messages/hi/yii.php new file mode 100644 index 0000000..e43b4bf --- /dev/null +++ b/framework/messages/hi/yii.php @@ -0,0 +1,138 @@ + ' और ', + '(not set)' => '(स्थापित नहीं)', + 'An internal server error occurred.' => 'सर्वर में एक आंतरिक दोष उत्पन्न हुआ है।', + 'Are you sure you want to delete this item?' => 'क्या आप सुनिश्चित रूप से इस आइटम को मिटाना चाहते हैं?', + 'Delete' => 'मिटाएँ', + 'Error' => 'खामी', + 'File upload failed.' => 'फ़ाइल अपलोड असफल रहा।', + 'Home' => 'घर', + 'Invalid data received for parameter "{param}".' => 'पैरामीटर "{param}" के लिए प्राप्त डेटा अमान्य है।', + 'Login Required' => 'लॉगिन आवश्यक हैं', + 'Missing required arguments: {params}' => 'आवश्यक तर्क: {params} अनुपस्थित है', + 'Missing required parameters: {params}' => 'आवश्यक पैरामीटर: {params} अनुपस्थित है', + 'No' => 'नहीं', + 'No results found.' => 'कोई परिणाम नहीं मिला।', + 'Only files with these MIME types are allowed: {mimeTypes}.' => 'केवल इन MIME प्रकारों वाली फ़ाइलों की अनुमति है: {mimeTypes}।', + 'Only files with these extensions are allowed: {extensions}.' => 'केवल इन एक्सटेंशन वाली फाइलों की अनुमति है: {extensions}।', + 'Page not found.' => 'पृष्ठ नहीं मिला।', + 'Please fix the following errors:' => 'कृपया निम्नलिखित खामीयां सुधारें:', + 'Please upload a file.' => 'कृपया एक फ़ाइल अपलोड करें।', + 'Powered by {yii}' => '{yii} द्वारा संचालित', + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'दिखाया गया है {totalCount, number} {totalCount, plural, one{चीज} other{चीज़े}} में से {begin, number}-{end, number} ।', + 'The combination {values} of {attributes} has already been taken.' => '{attributes} और {values} का संयोजन पहले से ही लिया जा चुका है।', + 'The file "{file}" is not an image.' => 'यह फ़ाइल "{file}" एक चित्र नहीं है।', + 'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'यह फ़ाइल "{file}" बहुत बड़ी है। इसका आकार {formattedLimit} से अधिक नहीं हो सकता है।', + 'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => 'यह फ़ाइल "{file}" बहुत छोटी है। इसका आकार {formattedLimit} से छोटा नहीं हो सकता।', + 'The format of {attribute} is invalid.' => '{attribute} का प्रारूप गलत है।', + 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'यह चित्र "{file}" बहुत बड़ी है। ऊंचाई {limit, number} {limit, plural, one{पिक्सेल} other{पिक्सेल}} से बड़ी नहीं हो सकती।', + 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'यह चित्र "{file}" बहुत बड़ी है। चौड़ाई {limit, number} {limit, plural, one{पिक्सेल} other{पिक्सेल}} से बड़ी नहीं हो सकती।', + 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'यह चित्र "{file}" बहुत छोटी है। ऊंचाई {limit, number} {limit, plural, one{पिक्सेल} other{पिक्सेल}} से छोटी नहीं हो सकती।', + 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'यह चित्र "{file}" बहुत छोटी है। चौड़ाई {limit, number} {limit, plural, one{पिक्सेल} other{पिक्सेल}} से छोटी नहीं हो सकती।', + 'The requested view "{name}" was not found.' => 'अनुरोधित दृश्य "{name}" नहीं मिला।', + 'The verification code is incorrect.' => 'सत्यापन कोड गलत है।', + 'Total {count, number} {count, plural, one{item} other{items}}.' => 'कुल {count, number} {count, plural, one{चीज} other{चीज़े}}।', + 'Unable to verify your data submission.' => 'आपके डेटा सबमिशन को सत्यापित करने में असमर्थ।', + 'Unknown alias: -{name}' => 'अज्ञात उपनाम: - {name}', + 'Unknown option: --{name}' => 'अज्ञात विकल्प: - {name}', + 'Update' => 'अपडेट करें', + 'View' => 'देखें', + 'Yes' => 'हाँ', + 'Yii Framework' => 'Yii फ़्रेमवर्क', + 'You are not allowed to perform this action.' => 'आपको यह करने की अनुमति नहीं है।', + 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'आप अधिकतम {limit, number} {limit, plural, one{फ़ाइल} other{फाइलें}} अपलोड कर सकते हैं।', + 'in {delta, plural, =1{a day} other{# days}}' => '{delta, plural, =1{एक दिन} other{# दिनों}} में', + 'in {delta, plural, =1{a minute} other{# minutes}}' => '{delta, plural, =1{एक मिनट} other{# मिनटों}} में', + 'in {delta, plural, =1{a month} other{# months}}' => '{delta, plural, =1{एक महीना} other{# महीनों}} में', + 'in {delta, plural, =1{a second} other{# seconds}}' => '{delta, plural, =1{एक सेकन्ड} other{# सेकन्डे}} में', + 'in {delta, plural, =1{a year} other{# years}}' => '{delta, plural, =1{एक वर्ष} other{# वर्षों}} में', + 'in {delta, plural, =1{an hour} other{# hours}}' => '{delta, plural, =1{एक घंटा} other{# घंटे}} में', + 'just now' => 'अभी', + 'the input value' => 'इनपुट मूल्य', + '{attribute} "{value}" has already been taken.' => '{attribute} "{value}" पहले से ही ले लिया गया है।', + '{attribute} cannot be blank.' => '{attribute} खाली नहीं हो सकता।', + '{attribute} contains wrong subnet mask.' => '{attribute} में गलत सबनेट मास्क है।', + '{attribute} is invalid.' => '{attribute} अमान्य है।', + '{attribute} is not a valid URL.' => '{attribute} एक मान्य URL नहीं है।', + '{attribute} is not a valid email address.' => '{attribute} एक मान्य ईमेल एड्रेस नहीं है।', + '{attribute} is not in the allowed range.' => '{attribute} अनुमत सीमा में नहीं है।', + '{attribute} must be "{requiredValue}".' => '{attribute} "{requiredValue}" होना चाहिए।', + '{attribute} must be a number.' => '{attribute} एक संख्या होनी चाहिए।', + '{attribute} must be a string.' => '{attribute} एक या ज़्यादा अक्षर होने चाहिए।', + '{attribute} must be a valid IP address.' => '{attribute} एक वैध IP एड्रेस होना चाहिए।', + '{attribute} must be an IP address with specified subnet.' => '{attribute} निर्दिष्ट सबनेट वाला IP एड्रेस होना चाहिए।', + '{attribute} must be an integer.' => '{attribute} एक पूर्णांक होनी चाहिए।', + '{attribute} must be either "{true}" or "{false}".' => '{attribute} या तो "{true}" या "{false}" होनी चाहिए।', + '{attribute} must be equal to "{compareValueOrAttribute}".' => '{attribute} "{compareValueOrAttribute}" के बराबर होनी चाहिए।', + '{attribute} must be greater than "{compareValueOrAttribute}".' => '{attribute} "{compareValueOrAttribute}" से अधिक होनी चाहिए।', + '{attribute} must be greater than or equal to "{compareValueOrAttribute}".' => '"{attribute}" {compareValueOrAttribute} "से अधिक या बराबर होनी चाहिए।', + '{attribute} must be less than "{compareValueOrAttribute}".' => '{attribute} "{compareValueOrAttribute}" से कम होनी चाहिए।', + '{attribute} must be less than or equal to "{compareValueOrAttribute}".' => '"{attribute}" {compareValueOrAttribute} "से कम या बराबर होनी चाहिए।', + '{attribute} must be no greater than {max}.' => '{attribute} {max} से अधिक नहीं होनी चाहिए।', + '{attribute} must be no less than {min}.' => '{attribute} {min} से कम नहीं होनी चाहिए।', + '{attribute} must not be a subnet.' => '{attribute} सबनेट नहीं होना चाहिए।', + '{attribute} must not be an IPv4 address.' => '{attribute} IPv4 एड्रेस नहीं होना चाहिए।', + '{attribute} must not be an IPv6 address.' => '{attribute} IPv6 एड्रेस नहीं होना चाहिए।', + '{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute} को "{compareValueOrAttribute}" के बराबर नहीं होना चाहिए।', + '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} में कम से कम {min, number} {min, plural, one{अक्षर} other{अक्षर}} होना चाहिए।', + '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} में अधिकतम {max, number} {max, plural, one{अक्षर} other{अक्षर}} होना चाहिए।', + '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} में {length, number} {length, plural, one{अक्षर} other{अक्षर}} शामिल होना चाहिए।', + '{delta, plural, =1{1 day} other{# days}}' => '{delta, plural, =1{1 दिन} other{# दिन}}', + '{delta, plural, =1{1 hour} other{# hours}}' => '{delta, plural, =1{1 घंटा} other{# घंटे}}', + '{delta, plural, =1{1 minute} other{# minutes}}' => '{delta, plural, =1{1 मिनट} other{# मिनिटे}}', + '{delta, plural, =1{1 month} other{# months}}' => '{delta, plural, =1{1 महीना} other{# महीने}}', + '{delta, plural, =1{1 second} other{# seconds}}' => '{delta, plural, =1{1 सेकंड} other{# सेकंड}}', + '{delta, plural, =1{1 year} other{# years}}' => '{delta, plural, =1{1 वर्ष} other{# वर्ष}}', + '{delta, plural, =1{a day} other{# days}} ago' => '{delta, plural, =1{एक दिन} other{# दिन}} पहले', + '{delta, plural, =1{a minute} other{# minutes}} ago' => '{delta, plural, =1{एक मिनट} other{# मिनट}} पहले', + '{delta, plural, =1{a month} other{# months}} ago' => '{delta, plural, =1{एक महीना} other{# महीने}} पहले', + '{delta, plural, =1{a second} other{# seconds}} ago' => '{delta, plural, =1{एक सेकंड} other{# सेकंड}} पहले', + '{delta, plural, =1{a year} other{# years}} ago' => '{delta, plural, =1{एक वर्ष} other{# वर्ष}} पहले', + '{delta, plural, =1{an hour} other{# hours}} ago' => '{delta, plural, =1{एक घंटा} other{# घंटे}} पहले', + '{nFormatted} B' => '{nFormatted} B', + '{nFormatted} GB' => '{nFormatted} GB', + '{nFormatted} GiB' => '{nFormatted} GiB', + '{nFormatted} kB' => '{nFormatted} KB', + '{nFormatted} KiB' => '{nFormatted} KiB', + '{nFormatted} MB' => '{nFormatted} MB', + '{nFormatted} MiB' => '{nFormatted} MiB', + '{nFormatted} PB' => '{nFormatted} PB', + '{nFormatted} PiB' => '{nFormatted} PiB', + '{nFormatted} TB' => '{nFormatted} TB', + '{nFormatted} TiB' => '{nFormatted} TiB', + '{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} {n, plural, =1{बाइट} other{बाइट्स}}', + '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} {n, plural, =1{गिबिबाइट} other{गिबिबाइटस}}', + '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} {n, plural, =1{गीगाबाइट} other{गीगाबाइटस}}', + '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}' => '{nFormatted} {n, plural, =1{किबिबाइट} other{किबिबाइटस}}', + '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}' => '{nFormatted} {n, plural, =1{किलोबाइट} other{किलोबाइट्स}}', + '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}' => '{nFormatted} {n, plural, =1{मेबीबाइट} other{मेबीबाइटस}', + '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}' => '{nFormatted} {n, plural, =1{मेगाबाइट} other{मेगाबाइट्स}}', + '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}' => '{nFormatted} {n, plural, =1{पेबिबाइट} other{पेबिबाइटस}', + '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} {n, plural, =1{पेटाबाइट} other{पेटाबाइट्स}}', + '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} {n, plural, =1{तबीबाइट} other{तबीबाइटस}}', + '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} {n, plural, =1{टेराबाइट} other{टेराबाइट्स}}', +]; diff --git a/framework/messages/hr/yii.php b/framework/messages/hr/yii.php index cbe8ba4..5826527 100644 --- a/framework/messages/hr/yii.php +++ b/framework/messages/hr/yii.php @@ -87,9 +87,9 @@ return [ '{attribute} must be no less than {min}.' => '{attribute} ne smije biti manji od {min}.', '{attribute} must be repeated exactly.' => '{attribute} mora biti točno ponovljeno.', '{attribute} must not be equal to "{compareValue}".' => '{attribute} ne smije biti jednak "{compareValue}".', - '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} mora najmanje sadržavati {min, number} {min, plural, =1{znak} one{# znak} few{# znaka} many{# znakova} other{# znakova}}.', - '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} moze sadržavati najviše do {max, number} {max, plural, =1{znak} one{# znak} few{# znaka} many{# znakova} other{# znakova}}.', - '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} mora sadržavati {length, number} {length, plural, =1{znak} one{# znak} few{# znaka} many{# znakova} other{# znakova}}.', + '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} mora najmanje sadržavati {min, number} {min, plural, =1{znak} one{znak} few{znaka} many{znakova} other{znakova}}.', + '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} moze sadržavati najviše do {max, number} {max, plural, =1{znak} one{znak} few{znaka} many{znakova} other{znakova}}.', + '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} mora sadržavati {length, number} {length, plural, =1{znak} one{znak} few{znaka} many{znakova} other{znakova}}.', '{delta, plural, =1{a day} other{# days}} ago' => '{delta, plural, =1{dan} one{# dan} few{# dana} many{# dana} other{# dana}}', '{delta, plural, =1{a minute} other{# minutes}} ago' => '{delta, plural, =1{minuta} one{# minuta} few{# minute} many{# minuta} other{# minuta}}', '{delta, plural, =1{a month} other{# months}} ago' => '{delta, plural, =1{mjesec} one{# mjesec} few{# mjeseca} many{# mjeseci} other{# mjeseci}}', diff --git a/framework/messages/hu/yii.php b/framework/messages/hu/yii.php index 2f4fedf..5500ec0 100644 --- a/framework/messages/hu/yii.php +++ b/framework/messages/hu/yii.php @@ -60,6 +60,7 @@ return [ 'Unknown option: --{name}' => 'Ismeretlen kapcsoló: --{name}', 'Update' => 'Szerkesztés', 'View' => 'Megtekintés', + 'just now' => 'éppen most', 'Yes' => 'Igen', 'You are not allowed to perform this action.' => 'Nincs jogosultsága a művelet végrehajtásához.', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Legfeljebb {limit, number} fájlt tölthet fel.', diff --git a/framework/messages/lv/yii.php b/framework/messages/lv/yii.php index 731618e..b94c28c 100644 --- a/framework/messages/lv/yii.php +++ b/framework/messages/lv/yii.php @@ -51,9 +51,9 @@ return [ 'Delete' => 'Dzēst', 'Error' => 'Kļūda', 'File upload failed.' => 'Neizdevās augšupielādēt datni.', - 'Home' => 'Galvenā', + 'Home' => 'Sākums', 'Invalid data received for parameter "{param}".' => 'Tika saņemta nepareiza vērtība parametram "{param}".', - 'Login Required' => 'Nepieciešama autorizācija.', + 'Login Required' => 'Nepieciešama autorizācija', 'Missing required arguments: {params}' => 'Trūkst nepieciešamie argumenti: {params}', 'Missing required parameters: {params}' => 'Trūkst nepieciešamie parametri: {params}', 'No' => 'Nē', @@ -63,8 +63,6 @@ return [ '{delta, plural, =1{1 month} other{# months}}' => '{delta, plural, zero{# mēneši} one{# mēnesis} other{# mēneši}}', '{delta, plural, =1{1 second} other{# seconds}}' => '{delta, plural, zero{# sekundes} one{# sekunde} other{# sekundes}}', '{delta, plural, =1{1 year} other{# years}}' => '{delta, plural, zero{# gadi} one{# gads} other{# gadi}}', - 'No help for unknown command "{command}".' => 'Palīdzība nezināmai komandai "{command}" nav pieejama.', - 'No help for unknown sub-command "{command}".' => 'Palīdzība nezināmai sub-komandai "{command}" nav pieejama', 'No results found.' => 'Nekas netika atrasts.', 'Only files with these MIME types are allowed: {mimeTypes}.' => 'Ir atļauts augšupielādēt datnes tikai ar šādiem MIME-tipiem: {mimeTypes}.', 'Only files with these extensions are allowed: {extensions}.' => 'Ir atļauts augšupielādēt datnes tikai ar šādiem paplašinājumiem: {extensions}.', @@ -72,20 +70,19 @@ return [ 'Please fix the following errors:' => 'Nepieciešams izlabot šādas kļūdas:', 'Please upload a file.' => 'Lūdzu, augšupielādēt datni.', 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'Tiek rādīti ieraksti {begin, number}-{end, number} no {totalCount, number}.', - 'The file "{file}" is not an image.' => 'Saņemtā „{file}” datne nav attēls.', - 'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'Saņemtās „{file}” datnes izmērs pārsniedz pieļaujamo ierobežojumu {formattedLimit} apmērā.', - 'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => 'Saņemtās „{file}” datnes izmērs ir pārāk maza, tai ir jābūt vismaz {formattedLimit} apmērā.', - 'The format of {attribute} is invalid.' => '„{attribute}” vērtības formāts ir nepareizs.', - 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls „{file}” ir pārāk liels. Augstumam ir jābūt mazākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.', - 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls „{file}” ir pārāk liels. Platumam ir jābūt mazākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.', - 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls „{file}” ir pārāk mazs. Augstumam ir jābūt lielākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.', - 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls „{file}” ir pārāk mazs. Platumam ir jābūt lielākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.', - 'The requested view "{name}" was not found.' => 'Pieprasītā skata datne „{name}” netika atrasta.', - 'The verification code is incorrect.' => 'Nepareizs pārbaudes kods.', + 'The file "{file}" is not an image.' => 'Saņemtā "{file}" datne nav attēls.', + 'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'Saņemtās "{file}" datnes izmērs pārsniedz pieļaujamo ierobežojumu {formattedLimit} apmērā.', + 'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => 'Saņemtās "{file}" datnes izmērs ir pārāk maza, tai ir jābūt vismaz {formattedLimit} apmērā.', + 'The format of {attribute} is invalid.' => '{attribute} vērtības formāts ir nepareizs.', + 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls "{file}" ir pārāk liels. Augstumam ir jābūt mazākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.', + 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls "{file}" ir pārāk liels. Platumam ir jābūt mazākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.', + 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls "{file}" ir pārāk mazs. Augstumam ir jābūt lielākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.', + 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls "{file}" ir pārāk mazs. Platumam ir jābūt lielākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.', + 'The requested view "{name}" was not found.' => 'Pieprasītā skata datne "{name}" netika atrasta.', + 'The verification code is incorrect.' => 'Cilvēktesta kods bija nepareizs.', 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Kopā {count, number} {count, plural, zero{ierakstu} one{ieraksts} other{ieraksti}}.', 'Unable to verify your data submission.' => 'Neizdevās apstiprināt saņemtos datus.', - 'Unknown command "{command}".' => 'Nezināma komanda "{command}".', - 'Unknown option: --{name}' => 'Nezināma izvēle: --{name}', + 'Unknown option: --{name}' => 'Nezināma iespēja: --{name}', 'Update' => 'Labot', 'View' => 'Apskatīt', 'Yes' => 'Jā', @@ -98,31 +95,54 @@ return [ 'in {delta, plural, =1{a year} other{# years}}' => 'pēc {delta, plural, =1{gada} one{# gada} other{# gadiem}}', 'in {delta, plural, =1{an hour} other{# hours}}' => 'pēc {delta, plural, =1{stundas} one{# stundas} other{# stundām}}', 'the input value' => 'ievadītā vērtība', - '{attribute} "{value}" has already been taken.' => '{attribute} „{value}” jau ir aizņemts.', - '{attribute} cannot be blank.' => 'Ir jāaizpilda „{attribute}”.', - '{attribute} is invalid.' => '„{attribute}” vērtība ir nepareiza.', - '{attribute} is not a valid URL.' => '„{attribute}” vērtība nav pareiza URL formātā.', - '{attribute} is not a valid email address.' => '„{attribute}” vērtība nav pareizas e-pasta adreses formātā.', - '{attribute} must be "{requiredValue}".' => '„{attribute}” vērtībai ir jābūt vienādai ar „{requiredValue}”.', - '{attribute} must be a number.' => '„{attribute}” vērtībai ir jābūt skaitlim.', - '{attribute} must be a string.' => '„{attribute}” vērtībai ir jābūt simbolu virknei.', - '{attribute} must be an integer.' => '„{attribute}” vērtībai ir jābūt veselam skaitlim.', - '{attribute} must be either "{true}" or "{false}".' => '„{attribute}” vērtībai ir jābūt „{true}” vai „{false}”.', - '{attribute} must be greater than "{compareValueOrAttribute}".' => '„{attribute}” vērtībai ir jābūt lielākai par „{compareValueOrAttribute}” vērtību.', - '{attribute} must be greater than or equal to "{compareValueOrAttribute}".' => '„{attribute}” vērtībai ir jābūt lielākai vai vienādai ar „{compareValueOrAttribute}” vērtību.', - '{attribute} must be less than "{compareValueOrAttribute}".' => '„{attribute}” vērtībai ir jābūt mazākai par „{compareValueOrAttribute}” vērtību.', - '{attribute} must be less than or equal to "{compareValueOrAttribute}".' => '„{attribute}” vērtībai ir jābūt mazākai vai vienādai ar „{compareValueOrAttribute}” vērtību.', - '{attribute} must be no greater than {max}.' => '„{attribute}” vērtībai ir jābūt ne lielākai par {max}.', - '{attribute} must be no less than {min}.' => '„{attribute}” vērtībai ir jābūt ne mazākai par {min}.', - '{attribute} must be equal to "{compareValueOrAttribute}".' => '„{attribute}” vērtībai ir jābūt vienādai ar „{compareValueOrAttribute}”.', - '{attribute} must not be equal to "{compareValueOrAttribute}".' => '„{attribute}” vērtība nedrīkst būt vienāda ar „{compareValueOrAttribute}” vērtību.', - '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '„{attribute}” vērtībai ir jābūt ne īsākai par {min, number} {min, plural, one{simbolu} other{simboliem}}.', - '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '„{attribute}” vērtībai ir jābūt ne garākai par {max, number} {max, plural, one{simbolu} other{simboliem}}.', - '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '„{attribute}” vērtībai ir jāsastāv no {length, number} {length, plural, one{simbola} other{simboliem}}.', + '{attribute} "{value}" has already been taken.' => '{attribute} "{value}" jau ir aizņemts.', + '{attribute} cannot be blank.' => 'Ir jāaizpilda {attribute}.', + '{attribute} is invalid.' => '{attribute} vērtība ir nepareiza.', + '{attribute} is not a valid URL.' => '{attribute} vērtība nav pareiza URL formātā.', + '{attribute} is not a valid email address.' => '{attribute} vērtība nav pareizas e-pasta adreses formātā.', + '{attribute} must be "{requiredValue}".' => '{attribute} vērtībai ir jābūt vienādai ar "{requiredValue}".', + '{attribute} must be a number.' => '{attribute} vērtībai ir jābūt skaitlim.', + '{attribute} must be a string.' => '{attribute} vērtībai ir jābūt simbolu virknei.', + '{attribute} must be an integer.' => '{attribute} vērtībai ir jābūt veselam skaitlim.', + '{attribute} must be either "{true}" or "{false}".' => '{attribute} vērtībai ir jābūt "{true}" vai "{false}".', + '{attribute} must be greater than "{compareValueOrAttribute}".' => '{attribute} vērtībai ir jābūt lielākai par "{compareValueOrAttribute}" vērtību.', + '{attribute} must be greater than or equal to "{compareValueOrAttribute}".' => '{attribute} vērtībai ir jābūt lielākai vai vienādai ar "{compareValueOrAttribute}" vērtību.', + '{attribute} must be less than "{compareValueOrAttribute}".' => '{attribute} vērtībai ir jābūt mazākai par "{compareValueOrAttribute}" vērtību.', + '{attribute} must be less than or equal to "{compareValueOrAttribute}".' => '{attribute} vērtībai ir jābūt mazākai vai vienādai ar "{compareValueOrAttribute}" vērtību.', + '{attribute} must be no greater than {max}.' => '{attribute} vērtībai ir jābūt ne lielākai par {max}.', + '{attribute} must be no less than {min}.' => '{attribute} vērtībai ir jābūt ne mazākai par {min}.', + '{attribute} must be equal to "{compareValueOrAttribute}".' => '{attribute} vērtībai ir jābūt vienādai ar "{compareValueOrAttribute}".', + '{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute} vērtība nedrīkst būt vienāda ar "{compareValueOrAttribute}" vērtību.', + '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} vērtībai ir jābūt ne īsākai par {min, number} {min, plural, one{simbolu} other{simboliem}}.', + '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} vērtībai ir jābūt ne garākai par {max, number} {max, plural, one{simbolu} other{simboliem}}.', + '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} vērtībai ir jāsastāv no {length, number} {length, plural, one{simbola} other{simboliem}}.', '{delta, plural, =1{a day} other{# days}} ago' => 'pirms {delta, plural, =1{dienas} one{# dienas} other{# dienām}}', '{delta, plural, =1{a minute} other{# minutes}} ago' => 'pirms {delta, plural, =1{minūtes} one{# minūtes} other{# minūtēm}}', '{delta, plural, =1{a month} other{# months}} ago' => 'pirms {delta, plural, =1{mēneša} one{# mēneša} other{# mēnešiem}}', '{delta, plural, =1{a second} other{# seconds}} ago' => 'pirms {delta, plural, =1{sekundes} one{# sekundes} other{# sekundēm}}', '{delta, plural, =1{a year} other{# years}} ago' => 'pirms {delta, plural, =1{gada} one{# gada} other{# gadiem}}', '{delta, plural, =1{an hour} other{# hours}} ago' => 'pirms {delta, plural, =1{stundas} one{# stundas} other{# stundām}}', + ' and ' => ' un ', + '"{attribute}" does not support operator "{operator}".' => '"{attribute}" neatbalsta operātoru "{operator}".', + 'Action not found.' => 'Darbība nav atrasta', + 'Aliases available: {aliases}' => 'Pieejamie pseidonīmi: {aliases}', + 'Condition for "{attribute}" should be either a value or valid operator specification.' => '"{attribute}" nosacījumam jābūt vai nu vērtībai, vai derīgai operatora specifikācijai.', + 'Operator "{operator}" must be used with a search attribute.' => 'Operātoru "{operator}" jāizmanto meklēšanas atribūtā', + 'Operator "{operator}" requires multiple operands.' => 'Operātoram "{operator}" nepieciešami vairāki operandi', + 'Options available: {options}' => 'Pieejamas opvijas: {options}', + 'Powered by {yii}' => 'Darbojas ar {yii}', + 'The combination {values} of {attributes} has already been taken.' => 'Kombinācija {values} priekš {attributes} ir jau aizņemta.', + 'The format of {filter} is invalid.' => '{filter} formāts ir kļūdains', + 'Unknown alias: -{name}' => 'Neatpazīts preidonīms {name}', + 'Unknown filter attribute "{attribute}"' => 'Neatpazīts filtra attribūts "{attribute}"', + 'Yii Framework' => 'Yii ietvars', + 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => 'Jums jāaugšupielādē vismaz {limit, number} {limit, mulural, one {file} other {files}}.', + 'just now' => 'tikko', + '{attribute} contains wrong subnet mask.' => '{attribute} satur kļūdainu apakštīklu.', + '{attribute} is not in the allowed range.' => '{attribute} nav atļautajā diapazonā.', + '{attribute} must be a valid IP address.' => '{attribute} jābūt derīgai IP adresei.', + '{attribute} must be an IP address with specified subnet.' => '{attribute} jābūt IP adresei ar norādīto apakštīklu.', + '{attribute} must not be a subnet.' => '{attribute} nedrīkst būt apakštīkls.', + '{attribute} must not be an IPv4 address.' => '{attribute} nedrīkst būt IPv4 adrese.', + '{attribute} must not be an IPv6 address.' => '{attribute} nedrīkst būt IPv6 adrese.', ]; diff --git a/framework/messages/pt/yii.php b/framework/messages/pt/yii.php index 0be0059..93aebcb 100644 --- a/framework/messages/pt/yii.php +++ b/framework/messages/pt/yii.php @@ -74,7 +74,7 @@ return [ '{nFormatted} B' => '{nFormatted} B', '{nFormatted} GB' => '{nFormatted} GB', '{nFormatted} GiB' => '{nFormatted} GiB', - '{nFormatted} KB' => '{nFormatted} kB', + '{nFormatted} kB' => '{nFormatted} kB', '{nFormatted} KiB' => '{nFormatted} KiB', '{nFormatted} MB' => '{nFormatted} MB', '{nFormatted} MiB' => '{nFormatted} MiB', diff --git a/framework/messages/ru/yii.php b/framework/messages/ru/yii.php index aa11574..4cbcf38 100644 --- a/framework/messages/ru/yii.php +++ b/framework/messages/ru/yii.php @@ -141,5 +141,5 @@ return [ '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} {n, plural, one{петабайт} few{петабайта} many{петабайтов} other{петабайта}}', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} {n, plural, one{тебибайт} few{тебибайта} many{тебибайтов} other{тебибайта}}', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} {n, plural, one{терабайт} few{терабайта} many{терабайтов} other{терабайта}}', - 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => 'Вы должны загрузить как минимум {limit, number} {one{файл} few{файла} many{файлов} other{файла}}.', + 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => 'Вы должны загрузить как минимум {limit, number} {limit, plural, one{файл} few{файла} many{файлов} other{файла}}.', ]; diff --git a/framework/messages/sk/yii.php b/framework/messages/sk/yii.php index d054314..3ca97ad 100644 --- a/framework/messages/sk/yii.php +++ b/framework/messages/sk/yii.php @@ -23,7 +23,6 @@ * NOTE: this file must be saved in UTF-8 encoding. */ return [ - 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => 'Je potrebné nahrať aspoň {limit, number} {limit, plural, =1{súbor} =2{súbory} =3{súbory} =4{súbory} other{súborov}}.', ' and ' => ' a ', '"{attribute}" does not support operator "{operator}".' => '"{attribute}" nepodporuje operátor "{operator}".', '(not set)' => '(nie je nastavené)', @@ -72,6 +71,7 @@ return [ 'Yii Framework' => 'Yii Framework', 'You are not allowed to perform this action.' => 'Nemáte oprávnenie pre požadovanú akciu.', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Nahrať môžete najviac {limit, number} {limit, plural, =1{súbor} =2{súbory} =3{súbory} =4{súbory} other{súborov}}.', + 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => 'Je potrebné nahrať aspoň {limit, number} {limit, plural, =1{súbor} =2{súbory} =3{súbory} =4{súbory} other{súborov}}.', 'in {delta, plural, =1{a day} other{# days}}' => 'o {delta, plural, =1{deň} =2{dni} =3{dni} =4{dni} other{# dní}}', 'in {delta, plural, =1{a minute} other{# minutes}}' => 'o {delta, plural, =1{minútu} =2{minúty} =3{minúty} =4{minúty} other{# minút}}', 'in {delta, plural, =1{a month} other{# months}}' => 'o {delta, plural, =1{mesiac} =2{mesiace} =3{mesiace} =4{mesiace} other{# mesiacov}}', @@ -123,7 +123,6 @@ return [ '{nFormatted} B' => '{nFormatted} B', '{nFormatted} GB' => '{nFormatted} GB', '{nFormatted} GiB' => '{nFormatted} GiB', - '{nFormatted} kB' => '{nFormatted} kB', '{nFormatted} KiB' => '{nFormatted} KiB', '{nFormatted} MB' => '{nFormatted} MB', '{nFormatted} MiB' => '{nFormatted} MiB', @@ -131,6 +130,7 @@ return [ '{nFormatted} PiB' => '{nFormatted} PiB', '{nFormatted} TB' => '{nFormatted} TB', '{nFormatted} TiB' => '{nFormatted} TiB', + '{nFormatted} kB' => '{nFormatted} kB', '{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} {n, plural, =1{bajt} =2{bajty} =3{bajty} =4{bajty} other{bajtov}}', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} {n, plural, =1{gibibajt} =2{gibibajty} =3{gibibajty} =4{gibibajty} other{gibibajtov}}', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} {n, plural, =1{gigabajt} =2{gigabajty} =3{gigabajty} =4{gigabajty} other{gigabajtov}}', @@ -142,4 +142,7 @@ return [ '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} {n, plural, =1{petabajt} =2{petabajty} =3{petabajty} =4{petabajty} other{petabajtov}}', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} {n, plural, =1{tebibajt} =2{tebibajty} =3{tebibajty} =4{tebibajty} other{tebibajtov}}', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} {n, plural, =1{terabajt} =2{terabajty} =3{terabajty} =4{terabajty} other{terabajtov}}', + 'Action not found.' => 'Akcia nebola nájdená.', + 'Aliases available: {aliases}' => 'Dostupné aliasy: {aliases}', + 'Options available: {options}' => 'Dostupné možnosti: {options}', ]; diff --git a/framework/messages/uz/yii.php b/framework/messages/uz/yii.php index 7d8433a..1d3a94d 100644 --- a/framework/messages/uz/yii.php +++ b/framework/messages/uz/yii.php @@ -108,9 +108,10 @@ return [ '{attribute} must be less than "{compareValue}".' => '«{attribute}» qiymati «{compareValue}» dan kichkina bo`lishi kerak.', '{attribute} must be less than or equal to "{compareValue}".' => '«{attribute}» qiymati «{compareValue}» dan kichik yoki teng bo`lishi kerak.', '{attribute} must be no greater than {max}.' => '«{attribute}» qiymati {max} dan oshmasligi kerak.', - '{attribute} must be no less than {min}.' => '«{attribute}» qiymati {min} dan kichkina bo`lishi kerak.', + '{attribute} must be no less than {min}.' => '«{attribute}» qiymati {min} dan kichkina bo`lmasligi kerak.', '{attribute} must be repeated exactly.' => '«{attribute}» qiymati bir xil tarzda takrorlanishi kerak.', '{attribute} must not be equal to "{compareValue}".' => '«{attribute}» qiymati «{compareValue}» ga teng bo`lmasligi kerak.', + '{attribute} must not be equal to "{compareValueOrAttribute}".' => '«{attribute}» qiymati «{compareValueOrAttribute}» qiymatiga teng bo`lmasligi lozim.', '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '«{attribute}» qiymati minimum {min, number} {min, plural, one{belgidan} few{belgidan} many{belgidan} other{belgidan}} tashkil topishi kerak.', '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '«{attribute}» qiymati maksimum {max, number} {max, plural, one{belgidan} few{belgidan} many{belgidan} other{belgidan}} oshmasligi kerak.', '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '«{attribute}» qiymati {length, number} {length, plural, one{belgidan} few{belgidan} many{belgidan} other{belgidan}} tashkil topishi kerak.', diff --git a/framework/mutex/Mutex.php b/framework/mutex/Mutex.php index 9bbd114..cfa6834 100644 --- a/framework/mutex/Mutex.php +++ b/framework/mutex/Mutex.php @@ -98,6 +98,18 @@ abstract class Mutex extends Component } /** + * Checks if a lock is currently acquired + * + * @param string $name of the lock to check + * @return bool Returns true if currently acquired + * @since 2.0.36 + */ + public function isAcquired($name) + { + return in_array($name, $this->_locks, true); + } + + /** * This method should be extended by a concrete Mutex implementations. Acquires lock by name. * @param string $name of the lock to be acquired. * @param int $timeout time (in seconds) to wait for the lock to be released. diff --git a/framework/rbac/BaseManager.php b/framework/rbac/BaseManager.php index 4f832e0..c8dda76 100644 --- a/framework/rbac/BaseManager.php +++ b/framework/rbac/BaseManager.php @@ -17,10 +17,10 @@ use yii\base\InvalidValueException; * * For more details and usage information on DbManager, see the [guide article on security authorization](guide:security-authorization). * - * @property Role[] $defaultRoleInstances Default roles. The array is indexed by the role names. This property - * is read-only. + * @property-read Role[] $defaultRoleInstances Default roles. The array is indexed by the role names. This + * property is read-only. * @property string[] $defaultRoles Default roles. Note that the type of this property differs in getter and - * setter. See [[getDefaultRoles()]] and [[setDefaultRoles()]] for details. + * setter. See [[getDefaultRoles()]] and [[setDefaultRoles()]] for details. * * @author Qiang Xue * @since 2.0 diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php index 141dacb..9aa29ae 100644 --- a/framework/rbac/DbManager.php +++ b/framework/rbac/DbManager.php @@ -100,6 +100,11 @@ class DbManager extends BaseManager * @var array auth item parent-child relationships (childName => list of parents) */ protected $parents; + /** + * @var array user assignments (user id => Assignment[]) + * @since `protected` since 2.0.38 + */ + protected $checkAccessAssignments = []; /** @@ -115,18 +120,16 @@ class DbManager extends BaseManager } } - private $_checkAccessAssignments = []; - /** * {@inheritdoc} */ public function checkAccess($userId, $permissionName, $params = []) { - if (isset($this->_checkAccessAssignments[(string) $userId])) { - $assignments = $this->_checkAccessAssignments[(string) $userId]; + if (isset($this->checkAccessAssignments[(string) $userId])) { + $assignments = $this->checkAccessAssignments[(string) $userId]; } else { $assignments = $this->getAssignments($userId); - $this->_checkAccessAssignments[(string) $userId] = $assignments; + $this->checkAccessAssignments[(string) $userId] = $assignments; } if ($this->hasNoAssignments($assignments)) { @@ -294,7 +297,7 @@ class DbManager extends BaseManager { if (!$this->supportsCascadeUpdate()) { $this->db->createCommand() - ->delete($this->itemChildTable, ['or', '[[parent]]=:name', '[[child]]=:name'], [':name' => $item->name]) + ->delete($this->itemChildTable, ['or', '[[parent]]=:parent', '[[child]]=:child'], [':parent' => $item->name, ':child' => $item->name]) ->execute(); $this->db->createCommand() ->delete($this->assignmentTable, ['item_name' => $item->name]) @@ -857,7 +860,7 @@ class DbManager extends BaseManager 'created_at' => $assignment->createdAt, ])->execute(); - unset($this->_checkAccessAssignments[(string) $userId]); + unset($this->checkAccessAssignments[(string) $userId]); return $assignment; } @@ -870,7 +873,7 @@ class DbManager extends BaseManager return false; } - unset($this->_checkAccessAssignments[(string) $userId]); + unset($this->checkAccessAssignments[(string) $userId]); return $this->db->createCommand() ->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name]) ->execute() > 0; @@ -885,7 +888,7 @@ class DbManager extends BaseManager return false; } - unset($this->_checkAccessAssignments[(string) $userId]); + unset($this->checkAccessAssignments[(string) $userId]); return $this->db->createCommand() ->delete($this->assignmentTable, ['user_id' => (string) $userId]) ->execute() > 0; @@ -970,7 +973,7 @@ class DbManager extends BaseManager */ public function removeAllAssignments() { - $this->_checkAccessAssignments = []; + $this->checkAccessAssignments = []; $this->db->createCommand()->delete($this->assignmentTable)->execute(); } @@ -982,7 +985,7 @@ class DbManager extends BaseManager $this->rules = null; $this->parents = null; } - $this->_checkAccessAssignments = []; + $this->checkAccessAssignments = []; } public function loadFromCache() diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php index 39fdcf1..9a15990 100644 --- a/framework/rbac/PhpManager.php +++ b/framework/rbac/PhpManager.php @@ -796,7 +796,7 @@ class PhpManager extends BaseManager */ protected function saveToFile($data, $file) { - file_put_contents($file, "invalidateScriptCache($file); } diff --git a/framework/rbac/migrations/m140506_102106_rbac_init.php b/framework/rbac/migrations/m140506_102106_rbac_init.php index e94afec..1429486 100644 --- a/framework/rbac/migrations/m140506_102106_rbac_init.php +++ b/framework/rbac/migrations/m140506_102106_rbac_init.php @@ -144,9 +144,10 @@ class m140506_102106_rbac_init extends \yii\db\Migration { $authManager = $this->getAuthManager(); $this->db = $authManager->db; + $schema = $this->db->getSchema()->defaultSchema; if ($this->isMSSQL()) { - $this->execute('DROP TRIGGER {$schema}.trigger_auth_item_child;'); + $this->execute("DROP TRIGGER {$schema}.trigger_auth_item_child;"); } $this->dropTable($authManager->assignmentTable); diff --git a/framework/rbac/migrations/m180523_151638_rbac_updates_indexes_without_prefix.php b/framework/rbac/migrations/m180523_151638_rbac_updates_indexes_without_prefix.php index f5882eb..ca85f46 100644 --- a/framework/rbac/migrations/m180523_151638_rbac_updates_indexes_without_prefix.php +++ b/framework/rbac/migrations/m180523_151638_rbac_updates_indexes_without_prefix.php @@ -39,6 +39,7 @@ class m180523_151638_rbac_updates_indexes_without_prefix extends Migration public function up() { $authManager = $this->getAuthManager(); + $this->db = $authManager->db; $this->dropIndex('auth_assignment_user_id_idx', $authManager->assignmentTable); $this->createIndex('{{%idx-auth_assignment-user_id}}', $authManager->assignmentTable, 'user_id'); @@ -53,6 +54,7 @@ class m180523_151638_rbac_updates_indexes_without_prefix extends Migration public function down() { $authManager = $this->getAuthManager(); + $this->db = $authManager->db; $this->dropIndex('{{%idx-auth_assignment-user_id}}', $authManager->assignmentTable); $this->createIndex('auth_assignment_user_id_idx', $authManager->assignmentTable, 'user_id'); diff --git a/framework/rbac/migrations/m200409_110543_rbac_update_mssql_trigger.php b/framework/rbac/migrations/m200409_110543_rbac_update_mssql_trigger.php new file mode 100644 index 0000000..9444582 --- /dev/null +++ b/framework/rbac/migrations/m200409_110543_rbac_update_mssql_trigger.php @@ -0,0 +1,164 @@ + + * @since 2.0.35 + */ +class m200409_110543_rbac_update_mssql_trigger extends Migration +{ + /** + * @throws yii\base\InvalidConfigException + * @return DbManager + */ + protected function getAuthManager() + { + $authManager = Yii::$app->getAuthManager(); + if (!$authManager instanceof DbManager) { + throw new InvalidConfigException('You should configure "authManager" component to use database before executing this migration.'); + } + + return $authManager; + } + + protected function findForeignKeyName($table, $column, $referenceTable, $referenceColumn) + { + return (new Query()) + ->select(['OBJECT_NAME(fkc.constraint_object_id)']) + ->from(['fkc' => 'sys.foreign_key_columns']) + ->innerJoin(['c' => 'sys.columns'], 'fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id') + ->innerJoin(['r' => 'sys.columns'], 'fkc.referenced_object_id = r.object_id AND fkc.referenced_column_id = r.column_id') + ->andWhere('fkc.parent_object_id=OBJECT_ID(:fkc_parent_object_id)',[':fkc_parent_object_id' => $this->db->schema->getRawTableName($table)]) + ->andWhere('fkc.referenced_object_id=OBJECT_ID(:fkc_referenced_object_id)',[':fkc_referenced_object_id' => $this->db->schema->getRawTableName($referenceTable)]) + ->andWhere(['c.name' => $column]) + ->andWhere(['r.name' => $referenceColumn]) + ->scalar($this->db); + } + + /** + * @return bool + */ + protected function isMSSQL() + { + return $this->db->driverName === 'mssql' || $this->db->driverName === 'sqlsrv' || $this->db->driverName === 'dblib'; + } + + /** + * {@inheritdoc} + */ + public function up() + { + if ($this->isMSSQL()) { + $authManager = $this->getAuthManager(); + $this->db = $authManager->db; + $schema = $this->db->getSchema()->defaultSchema; + $triggerSuffix = $this->db->schema->getRawTableName($authManager->itemChildTable); + + $this->execute("IF (OBJECT_ID(N'{$schema}.trigger_{$triggerSuffix}') IS NOT NULL) DROP TRIGGER {$schema}.trigger_{$triggerSuffix};"); + $this->execute("IF (OBJECT_ID(N'{$schema}.trigger_auth_item_child') IS NOT NULL) DROP TRIGGER {$schema}.trigger_auth_item_child;"); + + $this->execute("CREATE TRIGGER {$schema}.trigger_delete_{$triggerSuffix} + ON {$schema}.{$authManager->itemTable} + INSTEAD OF DELETE + AS + BEGIN + DELETE FROM {$schema}.{$authManager->itemChildTable} WHERE parent IN (SELECT name FROM deleted) OR child IN (SELECT name FROM deleted); + DELETE FROM {$schema}.{$authManager->itemTable} WHERE name IN (SELECT name FROM deleted); + END;" + ); + + $foreignKey = $this->findForeignKeyName($authManager->itemChildTable, 'child', $authManager->itemTable, 'name'); + $this->execute("CREATE TRIGGER {$schema}.trigger_update_{$triggerSuffix} + ON {$schema}.{$authManager->itemTable} + INSTEAD OF UPDATE + AS + DECLARE @old_name NVARCHAR(64) = (SELECT name FROM deleted) + DECLARE @new_name NVARCHAR(64) = (SELECT name FROM inserted) + BEGIN + IF @old_name <> @new_name + BEGIN + ALTER TABLE {$authManager->itemChildTable} NOCHECK CONSTRAINT {$foreignKey}; + UPDATE {$authManager->itemChildTable} SET child = @new_name WHERE child = @old_name; + END + UPDATE {$authManager->itemTable} + SET name = (SELECT name FROM inserted), + type = (SELECT type FROM inserted), + description = (SELECT description FROM inserted), + rule_name = (SELECT rule_name FROM inserted), + data = (SELECT data FROM inserted), + created_at = (SELECT created_at FROM inserted), + updated_at = (SELECT updated_at FROM inserted) + WHERE name IN (SELECT name FROM deleted) + IF @old_name <> @new_name + BEGIN + ALTER TABLE {$authManager->itemChildTable} CHECK CONSTRAINT {$foreignKey}; + END + END;" + ); + } + } + + /** + * {@inheritdoc} + */ + public function down() + { + if ($this->isMSSQL()) { + $authManager = $this->getAuthManager(); + $this->db = $authManager->db; + $schema = $this->db->getSchema()->defaultSchema; + $triggerSuffix = $this->db->schema->getRawTableName($authManager->itemChildTable); + + $this->execute("DROP TRIGGER {$schema}.trigger_update_{$triggerSuffix};"); + $this->execute("DROP TRIGGER {$schema}.trigger_delete_{$triggerSuffix};"); + + $this->execute("CREATE TRIGGER {$schema}.trigger_auth_item_child + ON {$schema}.{$authManager->itemTable} + INSTEAD OF DELETE, UPDATE + AS + DECLARE @old_name VARCHAR (64) = (SELECT name FROM deleted) + DECLARE @new_name VARCHAR (64) = (SELECT name FROM inserted) + BEGIN + IF COLUMNS_UPDATED() > 0 + BEGIN + IF @old_name <> @new_name + BEGIN + ALTER TABLE {$authManager->itemChildTable} NOCHECK CONSTRAINT FK__auth_item__child; + UPDATE {$authManager->itemChildTable} SET child = @new_name WHERE child = @old_name; + END + UPDATE {$authManager->itemTable} + SET name = (SELECT name FROM inserted), + type = (SELECT type FROM inserted), + description = (SELECT description FROM inserted), + rule_name = (SELECT rule_name FROM inserted), + data = (SELECT data FROM inserted), + created_at = (SELECT created_at FROM inserted), + updated_at = (SELECT updated_at FROM inserted) + WHERE name IN (SELECT name FROM deleted) + IF @old_name <> @new_name + BEGIN + ALTER TABLE {$authManager->itemChildTable} CHECK CONSTRAINT FK__auth_item__child; + END + END + ELSE + BEGIN + DELETE FROM {$schema}.{$authManager->itemChildTable} WHERE parent IN (SELECT name FROM deleted) OR child IN (SELECT name FROM deleted); + DELETE FROM {$schema}.{$authManager->itemTable} WHERE name IN (SELECT name FROM deleted); + END + END;"); + } + } +} diff --git a/framework/rest/Serializer.php b/framework/rest/Serializer.php index 8105eb3..2a47511 100644 --- a/framework/rest/Serializer.php +++ b/framework/rest/Serializer.php @@ -137,7 +137,7 @@ class Serializer extends Component * Serializes the given data into a format that can be easily turned into other formats. * This method mainly converts the objects of recognized types into array representation. * It will not do conversion for unknown object types or non-object data. - * The default implementation will handle [[Model]] and [[DataProviderInterface]]. + * The default implementation will handle [[Model]], [[DataProviderInterface]] and [\JsonSerializable](https://www.php.net/manual/en/class.jsonserializable.php). * You may override this method to support more object types. * @param mixed $data the data to be serialized. * @return mixed the converted data. @@ -148,8 +148,16 @@ class Serializer extends Component return $this->serializeModelErrors($data); } elseif ($data instanceof Arrayable) { return $this->serializeModel($data); + } elseif ($data instanceof \JsonSerializable) { + return $data->jsonSerialize(); } elseif ($data instanceof DataProviderInterface) { return $this->serializeDataProvider($data); + } elseif (is_array($data)) { + $serializedArray = []; + foreach ($data as $key => $value) { + $serializedArray[$key] = $this->serialize($value); + } + return $serializedArray; } return $data; diff --git a/framework/rest/UrlRule.php b/framework/rest/UrlRule.php index 73c5dc4..413ecf3 100644 --- a/framework/rest/UrlRule.php +++ b/framework/rest/UrlRule.php @@ -205,9 +205,6 @@ class UrlRule extends CompositeUrlRule $config['verb'] = $verbs; $config['pattern'] = rtrim($prefix . '/' . strtr($pattern, $this->tokens), '/'); $config['route'] = $action; - if (!empty($verbs) && !in_array('GET', $verbs)) { - $config['mode'] = WebUrlRule::PARSING_ONLY; - } $config['suffix'] = $this->suffix; return Yii::createObject($config); @@ -219,6 +216,10 @@ class UrlRule extends CompositeUrlRule public function parseRequest($manager, $request) { $pathInfo = $request->getPathInfo(); + if ($this->prefix !== '' && strpos($pathInfo . '/', $this->prefix . '/') !== 0) { + return false; + } + foreach ($this->rules as $urlName => $rules) { if (strpos($pathInfo, $urlName) !== false) { foreach ($rules as $rule) { diff --git a/framework/test/ActiveFixture.php b/framework/test/ActiveFixture.php index 6d93b79..ff626a4 100644 --- a/framework/test/ActiveFixture.php +++ b/framework/test/ActiveFixture.php @@ -24,7 +24,7 @@ use yii\db\TableSchema; * * For more details and usage information on ActiveFixture, see the [guide article on fixtures](guide:test-fixtures). * - * @property TableSchema $tableSchema The schema information of the database table associated with this + * @property-read TableSchema $tableSchema The schema information of the database table associated with this * fixture. This property is read-only. * * @author Qiang Xue diff --git a/framework/test/FixtureTrait.php b/framework/test/FixtureTrait.php index 49ca57f..e5dc9b0 100644 --- a/framework/test/FixtureTrait.php +++ b/framework/test/FixtureTrait.php @@ -165,12 +165,11 @@ trait FixtureTrait /** * Creates the specified fixture instances. - * All dependent fixtures will also be created. + * All dependent fixtures will also be created. Duplicate fixtures and circular dependencies will only be created once. * @param array $fixtures the fixtures to be created. You may provide fixture names or fixture configurations. * If this parameter is not provided, the fixtures specified in [[globalFixtures()]] and [[fixtures()]] will be created. * @return Fixture[] the created fixture instances - * @throws InvalidConfigException if fixtures are not properly configured or if a circular dependency among - * the fixtures is detected. + * @throws InvalidConfigException if fixtures are not properly configured */ protected function createFixtures(array $fixtures) { @@ -210,9 +209,8 @@ trait FixtureTrait // need to use the configuration provided in test case $stack[] = isset($config[$dep]) ? $config[$dep] : ['class' => $dep]; } - } elseif ($instances[$name] === false) { - throw new InvalidConfigException("A circular dependency is detected for fixture '$class'."); } + // if the fixture is already loaded (ie. a circular dependency or if two fixtures depend on the same fixture) just skip it. } } diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 8df351b..a57886d 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -146,6 +146,9 @@ class CompareValidator extends Validator return; } if ($this->compareValue !== null) { + if ($this->compareValue instanceof \Closure) { + $this->compareValue = call_user_func($this->compareValue); + } $compareLabel = $compareValue = $compareValueOrAttribute = $this->compareValue; } else { $compareAttribute = $this->compareAttribute === null ? $attribute . '_repeat' : $this->compareAttribute; @@ -170,6 +173,9 @@ class CompareValidator extends Validator if ($this->compareValue === null) { throw new InvalidConfigException('CompareValidator::compareValue must be set.'); } + if ($this->compareValue instanceof \Closure) { + $this->compareValue = call_user_func($this->compareValue); + } if (!$this->compareValues($this->operator, $this->type, $value, $this->compareValue)) { return [$this->message, [ 'compareAttribute' => $this->compareValue, @@ -225,6 +231,10 @@ class CompareValidator extends Validator */ public function clientValidateAttribute($model, $attribute, $view) { + if ($this->compareValue != null && $this->compareValue instanceof \Closure) { + $this->compareValue = call_user_func($this->compareValue); + } + ValidationAsset::register($view); $options = $this->getClientOptions($model, $attribute); diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index 3f2dccf..24604cf 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -8,6 +8,7 @@ namespace yii\validators; use DateTime; +use DateTimeZone; use Exception; use IntlDateFormatter; use Yii; @@ -125,10 +126,11 @@ class DateValidator extends Validator * This can be the same attribute as the one being validated. If this is the case, * the original value will be overwritten with the timestamp value after successful validation. * - * Note, that when using this property, the input value will be converted to a unix timestamp, - * which by definition is in UTC, so a conversion from the [[$timeZone|input time zone]] to UTC - * will be performed. When defining [[$timestampAttributeFormat]] you can control the conversion by - * setting [[$timestampAttributeTimeZone]] to a different value than `'UTC'`. + * Note, that when using this property, the input value will be converted to a unix timestamp, which by definition + * is in [[$defaultTimeZone|default UTC time zone]], so a conversion from the [[$timeZone|input time zone]] to + * the default one will be performed. If you want to change the default time zone, set the [[$defaultTimeZone]] property. + * When defining [[$timestampAttributeFormat]] you can further control the conversion by setting + * [[$timestampAttributeTimeZone]] to a different value than `'UTC'`. * * @see timestampAttributeFormat * @see timestampAttributeTimeZone @@ -146,7 +148,7 @@ class DateValidator extends Validator */ public $timestampAttributeFormat; /** - * @var string the timezone to use when populating the [[timestampAttribute]]. Defaults to `UTC`. + * @var string the timezone to use when populating the [[timestampAttribute]] with [[timestampAttributeFormat]]. Defaults to `UTC`. * * This can be any value that may be passed to [date_default_timezone_set()](https://secure.php.net/manual/en/function.date-default-timezone-set.php) * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`. @@ -202,6 +204,12 @@ class DateValidator extends Validator * @since 2.0.22 */ public $strictDateFormat = false; + /** + * @var string the default timezone used for parsing when no time parts are provided in the format. + * See [[timestampAttributeTimeZone]] for more description. + * @since 2.0.39 + */ + public $defaultTimeZone = 'UTC'; /** * @var array map of short format names to IntlDateFormatter constant values. @@ -288,10 +296,8 @@ class DateValidator extends Validator if (is_int($value)) { return; } - } else { - if ($this->parseDateValueFormat($value, $this->timestampAttributeFormat) !== false) { - return; - } + } elseif ($this->parseDateValueFormat($value, $this->timestampAttributeFormat) !== false) { + return; } } $this->addError($model, $attribute, $this->message, []); @@ -328,7 +334,7 @@ class DateValidator extends Validator /** * Parses date string into UNIX timestamp. * - * @param string $value string representing date + * @param mixed $value string representing date * @return int|false a UNIX timestamp or `false` on failure. */ protected function parseDateValue($value) @@ -340,7 +346,7 @@ class DateValidator extends Validator /** * Parses date string into UNIX timestamp. * - * @param string $value string representing date + * @param mixed $value string representing date * @param string $format expected date format * @return int|false a UNIX timestamp or `false` on failure. * @throws InvalidConfigException @@ -399,17 +405,15 @@ class DateValidator extends Validator private function getIntlDateFormatter($format) { if (!isset($this->_dateFormats[$format])) { - // if no time was provided in the format string set time to 0 to get a simple date timestamp - $hasTimeInfo = (strpbrk($format, 'ahHkKmsSA') !== false); - $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $hasTimeInfo ? $this->timeZone : 'UTC', null, $format); - - return $formatter; + // if no time was provided in the format string set timezone to default one to match yii\i18n\Formatter::formatDateTimeValue() + $timezone = strpbrk($format, 'ahHkKmsSA') !== false ? $this->timeZone : $this->defaultTimeZone; + return new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timezone, null, $format); } if ($this->type === self::TYPE_DATE) { $dateType = $this->_dateFormats[$format]; $timeType = IntlDateFormatter::NONE; - $timeZone = 'UTC'; + $timeZone = $this->defaultTimeZone; } elseif ($this->type === self::TYPE_DATETIME) { $dateType = $this->_dateFormats[$format]; $timeType = $this->_dateFormats[$format]; @@ -422,9 +426,7 @@ class DateValidator extends Validator throw new InvalidConfigException('Unknown validation type set for DateValidator::$type: ' . $this->type); } - $formatter = new IntlDateFormatter($this->locale, $dateType, $timeType, $timeZone); - - return $formatter; + return new IntlDateFormatter($this->locale, $dateType, $timeType, $timeZone); } /** @@ -435,16 +437,17 @@ class DateValidator extends Validator */ private function parseDateValuePHP($value, $format) { - // if no time was provided in the format string set time to 0 to get a simple date timestamp - $hasTimeInfo = (strpbrk($format, 'HhGgisU') !== false); - - $date = DateTime::createFromFormat($format, $value, new \DateTimeZone($hasTimeInfo ? $this->timeZone : 'UTC')); + $hasTimeInfo = strpbrk($format, 'HhGgisU') !== false; + // if no time was provided in the format string set timezone to default one to match yii\i18n\Formatter::formatDateTimeValue() + $timezone = $hasTimeInfo ? $this->timeZone : $this->defaultTimeZone; + $date = DateTime::createFromFormat($format, $value, new DateTimeZone($timezone)); $errors = DateTime::getLastErrors(); if ($date === false || $errors['error_count'] || $errors['warning_count'] || ($this->strictDateFormat && $date->format($format) !== $value)) { return false; } if (!$hasTimeInfo) { + // if no time was provided in the format string set time to 0 to get a simple date timestamp $date->setTime(0, 0, 0); } @@ -468,7 +471,8 @@ class DateValidator extends Validator $date = new DateTime(); $date->setTimestamp($timestamp); - $date->setTimezone(new \DateTimeZone($this->timestampAttributeTimeZone)); + $date->setTimezone(new DateTimeZone($this->timestampAttributeTimeZone)); + return $date->format($format); } } diff --git a/framework/validators/EachValidator.php b/framework/validators/EachValidator.php index e8c9ef4..504738d 100644 --- a/framework/validators/EachValidator.php +++ b/framework/validators/EachValidator.php @@ -8,6 +8,7 @@ namespace yii\validators; use Yii; +use yii\base\DynamicModel; use yii\base\InvalidConfigException; use yii\base\Model; @@ -70,11 +71,6 @@ class EachValidator extends Validator */ public $stopOnFirstError = true; - /** - * @var Validator validator instance. - */ - private $_validator; - /** * {@inheritdoc} @@ -88,36 +84,27 @@ class EachValidator extends Validator } /** - * Returns the validator declared in [[rule]]. - * @param Model|null $model model in which context validator should be created. - * @return Validator the declared validator. - */ - private function getValidator($model = null) - { - if ($this->_validator === null) { - $this->_validator = $this->createEmbeddedValidator($model); - } - - return $this->_validator; - } - - /** * Creates validator object based on the validation rule specified in [[rule]]. * @param Model|null $model model in which context validator should be created. + * @param mixed|null $current value being currently validated. * @throws \yii\base\InvalidConfigException * @return Validator validator instance */ - private function createEmbeddedValidator($model) + private function createEmbeddedValidator($model = null, $current = null) { $rule = $this->rule; if ($rule instanceof Validator) { return $rule; - } elseif (is_array($rule) && isset($rule[0])) { // validator type + } + + if (is_array($rule) && isset($rule[0])) { // validator type if (!is_object($model)) { $model = new Model(); // mock up context model } - return Validator::createValidator($rule[0], $model, $this->attributes, array_slice($rule, 1)); + $params = array_slice($rule, 1); + $params['current'] = $current; + return Validator::createValidator($rule[0], $model, $this->attributes, $params); } throw new InvalidConfigException('Invalid validation rule: a rule must be an array specifying validator type.'); @@ -128,43 +115,38 @@ class EachValidator extends Validator */ public function validateAttribute($model, $attribute) { - $value = $model->$attribute; - if (!is_array($value) && !$value instanceof \ArrayAccess) { + $arrayOfValues = $model->$attribute; + if (!is_array($arrayOfValues) && !$arrayOfValues instanceof \ArrayAccess) { $this->addError($model, $attribute, $this->message, []); return; } - $validator = $this->getValidator($model); // ensure model context while validator creation + foreach ($arrayOfValues as $k => $v) { + $dynamicModel = new DynamicModel($model->getAttributes()); + $dynamicModel->setAttributeLabels($model->attributeLabels()); + $dynamicModel->addRule($attribute, $this->createEmbeddedValidator($model, $v)); + $dynamicModel->defineAttribute($attribute, $v); + $dynamicModel->validate(); + + $arrayOfValues[$k] = $dynamicModel->$attribute; // filtered values like 'trim' + + if (!$dynamicModel->hasErrors($attribute)) { + continue; + } - $detectedErrors = $model->getErrors($attribute); - $filteredValue = $model->$attribute; - foreach ($value as $k => $v) { - $model->clearErrors($attribute); - $model->$attribute = $v; - if (!$validator->skipOnEmpty || !$validator->isEmpty($v)) { - $validator->validateAttribute($model, $attribute); + if ($this->allowMessageFromRule) { + $validationErrors = $dynamicModel->getErrors($attribute); + $model->addErrors([$attribute => $validationErrors]); + } else { + $this->addError($model, $attribute, $this->message, ['value' => $v]); } - $filteredValue[$k] = $model->$attribute; - if ($model->hasErrors($attribute)) { - if ($this->allowMessageFromRule) { - $validationErrors = $model->getErrors($attribute); - $detectedErrors = array_merge($detectedErrors, $validationErrors); - } else { - $model->clearErrors($attribute); - $this->addError($model, $attribute, $this->message, ['value' => $v]); - $detectedErrors[] = $model->getFirstError($attribute); - } - $model->$attribute = $value; - if ($this->stopOnFirstError) { - break; - } + if ($this->stopOnFirstError) { + break; } } - $model->$attribute = $filteredValue; - $model->clearErrors($attribute); - $model->addErrors([$attribute => $detectedErrors]); + $model->$attribute = $arrayOfValues; } /** @@ -176,7 +158,7 @@ class EachValidator extends Validator return [$this->message, []]; } - $validator = $this->getValidator(); + $validator = $this->createEmbeddedValidator(); foreach ($value as $v) { if ($validator->skipOnEmpty && $validator->isEmpty($v)) { continue; diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index dc97ad1..0418e5f 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -8,6 +8,7 @@ namespace yii\validators; use Yii; +use yii\base\ErrorException; use yii\base\InvalidConfigException; use yii\helpers\Json; use yii\web\JsExpression; @@ -111,21 +112,24 @@ class EmailValidator extends Validator */ protected function isDNSValid($domain) { - if (checkdnsrr($domain . '.', 'MX')) { - $mxRecords = dns_get_record($domain . '.', DNS_MX); - if ($mxRecords !== false && count($mxRecords) > 0) { - return true; - } + return $this->hasDNSRecord($domain, true) || $this->hasDNSRecord($domain, false); + } + + private function hasDNSRecord($domain, $isMX) + { + $normalizedDomain = $domain . '.'; + if (!checkdnsrr($normalizedDomain, ($isMX ? 'MX' : 'A'))) { + return false; } - if (checkdnsrr($domain . '.', 'A')) { - $aRecords = dns_get_record($domain . '.', DNS_A); - if ($aRecords !== false && count($aRecords) > 0) { - return true; - } + try { + // dns_get_record can return false and emit Warning that may or may not be converted to ErrorException + $records = dns_get_record($normalizedDomain, ($isMX ? DNS_MX : DNS_A)); + } catch (ErrorException $exception) { + return false; } - return false; + return !empty($records); } private function idnToAscii($idn) diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index 7b981cc..d67934f 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -11,6 +11,7 @@ use Yii; use yii\helpers\FileHelper; use yii\helpers\Html; use yii\helpers\Json; +use yii\helpers\StringHelper; use yii\web\JsExpression; use yii\web\UploadedFile; @@ -19,7 +20,7 @@ use yii\web\UploadedFile; * * Note that you should enable `fileinfo` PHP extension. * - * @property int $sizeLimit The size limit for uploaded files. This property is read-only. + * @property-read int $sizeLimit The size limit for uploaded files. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -412,7 +413,12 @@ class FileValidator extends Validator } } - if (!in_array($extension, $this->extensions, true)) { + if (!empty($this->extensions)) { + foreach ((array) $this->extensions as $ext) { + if ($extension === $ext || StringHelper::endsWith($file->name, ".$ext", false)) { + return true; + } + } return false; } diff --git a/framework/validators/ImageValidator.php b/framework/validators/ImageValidator.php index 9b03749..6edfb97 100644 --- a/framework/validators/ImageValidator.php +++ b/framework/validators/ImageValidator.php @@ -47,7 +47,7 @@ class ImageValidator extends FileValidator /** * @var int the maximum width in pixels. * Defaults to null, meaning no limit. - * @see overWidth for the customized message used when image height is too big. + * @see overHeight for the customized message used when image height is too big. */ public $maxHeight; /** diff --git a/framework/validators/InlineValidator.php b/framework/validators/InlineValidator.php index 93d41a1..a8dcd00 100644 --- a/framework/validators/InlineValidator.php +++ b/framework/validators/InlineValidator.php @@ -58,6 +58,11 @@ class InlineValidator extends Validator * Please refer to [[clientValidateAttribute()]] for details on how to return client validation code. */ public $clientValidate; + /** + * @var mixed the value of attribute being currently validated. + * @since 2.0.36 + */ + public $current; /** @@ -68,8 +73,15 @@ class InlineValidator extends Validator $method = $this->method; if (is_string($method)) { $method = [$model, $method]; + } elseif ($method instanceof \Closure) { + $method = $this->method->bindTo($model); + } + + $current = $this->current; + if ($current === null) { + $current = $model->$attribute; } - call_user_func($method, $attribute, $this->params, $this); + $method($attribute, $this->params, $this, $current); } /** @@ -81,9 +93,14 @@ class InlineValidator extends Validator $method = $this->clientValidate; if (is_string($method)) { $method = [$model, $method]; + } elseif ($method instanceof \Closure) { + $method = $method->bindTo($model); } - - return call_user_func($method, $attribute, $this->params, $this); + $current = $this->current; + if ($current === null) { + $current = $model->$attribute; + } + return $method($attribute, $this->params, $this, $current); } return null; diff --git a/framework/validators/IpValidator.php b/framework/validators/IpValidator.php index d34c03b..ea1e877 100644 --- a/framework/validators/IpValidator.php +++ b/framework/validators/IpValidator.php @@ -33,7 +33,8 @@ use yii\web\JsExpression; * ``` * * @property array $ranges The IPv4 or IPv6 ranges that are allowed or forbidden. See [[setRanges()]] for - * detailed description. + * detailed description. Note that the type of this property differs in getter and setter. See [[getRanges()]] + * and [[setRanges()]] for details. * * @author Dmitry Naumenko * @since 2.0.7 @@ -247,7 +248,7 @@ class IpValidator extends Validator * * @property array the IPv4 or IPv6 ranges that are allowed or forbidden. * See [[setRanges()]] for detailed description. - * @param array $ranges the IPv4 or IPv6 ranges that are allowed or forbidden. + * @param array|string $ranges the IPv4 or IPv6 ranges that are allowed or forbidden. * * When the array is empty, or the option not set, all IP addresses are allowed. * diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index c084804..646357a 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -34,9 +34,11 @@ class RequiredValidator extends Validator * @var bool whether the comparison between the attribute value and [[requiredValue]] is strict. * When this is true, both the values and types must match. * Defaults to false, meaning only the values need to match. - * Note that when [[requiredValue]] is null, if this property is true, the validator will check - * if the attribute value is null; If this property is false, the validator will call [[isEmpty]] - * to check if the attribute value is empty. + * + * Note that behavior for when [[requiredValue]] is null is the following: + * + * - In strict mode, the validator will check if the attribute value is null + * - In non-strict mode validation will fail */ public $strict = false; /** diff --git a/framework/validators/SafeValidator.php b/framework/validators/SafeValidator.php index f86f239..2b0a3c5 100644 --- a/framework/validators/SafeValidator.php +++ b/framework/validators/SafeValidator.php @@ -14,7 +14,7 @@ namespace yii\validators; * when a user submits form data to be loaded into a model directly from the POST data, is it ok for a property to be copied. * In many cases, this is required but because sometimes properties are internal and you do not want the POST data to be able to * override these internal values (especially things like database row ids), Yii assumes all values are unsafe for massive assignment - * unless a validation rule exists for the property, which in most cases it will. Sometimes, however, an item is safe for massive assigment but + * unless a validation rule exists for the property, which in most cases it will. Sometimes, however, an item is safe for massive assignment but * does not have a validation rule associated with it - for instance, due to no validation being performed, in which case, you use this class * as a validation rule for that property. Although it has no functionality, it allows Yii to determine that the property is safe to copy. * diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 6961ac5..f06dbfc 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -64,6 +64,12 @@ class StringValidator extends Validator * If this property is not set, [[\yii\base\Application::charset]] will be used. */ public $encoding; + /** + * @var boolean whether to require the value to be a string data type. + * If false any scalar value will be treated as it's string equivalent. + * @since 2.0.33 + */ + public $strict = true; /** @@ -104,7 +110,9 @@ class StringValidator extends Validator public function validateAttribute($model, $attribute) { $value = $model->$attribute; - + if (!$this->strict && is_scalar($value) && !is_string($value)) { + $value = (string)$value; + } if (!is_string($value)) { $this->addError($model, $attribute, $this->message); @@ -129,6 +137,10 @@ class StringValidator extends Validator */ protected function validateValue($value) { + if (!$this->strict && is_scalar($value) && !is_string($value)) { + $value = (string)$value; + } + if (!is_string($value)) { return [$this->message, []]; } diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index 7ef5419..5b808ac 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -48,8 +48,8 @@ use yii\base\NotSupportedException; * * For more details and usage information on Validator, see the [guide article on validators](guide:input-validation). * - * @property array $attributeNames Attribute names. This property is read-only. - * @property array $validationAttributes List of attribute names. This property is read-only. + * @property-read array $attributeNames Attribute names. This property is read-only. + * @property-read array $validationAttributes List of attribute names. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -209,11 +209,15 @@ class Validator extends Component { $params['attributes'] = $attributes; - if ($type instanceof \Closure || ($model->hasMethod($type) && !isset(static::$builtInValidators[$type]))) { - // method-based validator + if ($type instanceof \Closure) { $params['class'] = __NAMESPACE__ . '\InlineValidator'; $params['method'] = $type; + } elseif (!isset(static::$builtInValidators[$type]) && $model->hasMethod($type)) { + // method-based validator + $params['class'] = __NAMESPACE__ . '\InlineValidator'; + $params['method'] = [$model, $type]; } else { + unset($params['current']); if (isset(static::$builtInValidators[$type])) { $type = static::$builtInValidators[$type]; } diff --git a/framework/views/errorHandler/exception.php b/framework/views/errorHandler/exception.php index 319478f..d097a1c 100644 --- a/framework/views/errorHandler/exception.php +++ b/framework/views/errorHandler/exception.php @@ -531,8 +531,8 @@ window.onload = function() { }; // Highlight lines that have text in them but still support text selection: - document.onmousedown = function() { document.getElementsByTagName('body')[0].classList.add('mousedown'); } - document.onmouseup = function() { document.getElementsByTagName('body')[0].classList.remove('mousedown'); } + document.onmousedown = function() { document.getElementsByTagName('body')[0].classList.add('mousedown'); }; + document.onmouseup = function() { document.getElementsByTagName('body')[0].classList.remove('mousedown'); }; endBody() // to allow injecting code into body (mostly by Yii Debug Toolbar)?> diff --git a/framework/web/Application.php b/framework/web/Application.php index f20c2f8..6a052f1 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -16,12 +16,13 @@ use yii\helpers\Url; * * For more details and usage information on Application, see the [guide article on applications](guide:structure-applications). * - * @property ErrorHandler $errorHandler The error handler application component. This property is read-only. + * @property-read ErrorHandler $errorHandler The error handler application component. This property is + * read-only. * @property string $homeUrl The homepage URL. - * @property Request $request The request component. This property is read-only. - * @property Response $response The response component. This property is read-only. - * @property Session $session The session component. This property is read-only. - * @property User $user The user component. This property is read-only. + * @property-read Request $request The request component. This property is read-only. + * @property-read Response $response The response component. This property is read-only. + * @property-read Session $session The session component. This property is read-only. + * @property-read User $user The user component. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 9890da9..d04ad40 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -153,22 +153,18 @@ class AssetBundle extends BaseObject if (is_array($js)) { $file = array_shift($js); $options = ArrayHelper::merge($this->jsOptions, $js); - $view->registerJsFile($manager->getAssetUrl($this, $file), $options); - } else { - if ($js !== null) { - $view->registerJsFile($manager->getAssetUrl($this, $js), $this->jsOptions); - } + $view->registerJsFile($manager->getAssetUrl($this, $file, ArrayHelper::getValue($options, 'appendTimestamp')), $options); + } elseif ($js !== null) { + $view->registerJsFile($manager->getAssetUrl($this, $js), $this->jsOptions); } } foreach ($this->css as $css) { if (is_array($css)) { $file = array_shift($css); $options = ArrayHelper::merge($this->cssOptions, $css); - $view->registerCssFile($manager->getAssetUrl($this, $file), $options); - } else { - if ($css !== null) { - $view->registerCssFile($manager->getAssetUrl($this, $css), $this->cssOptions); - } + $view->registerCssFile($manager->getAssetUrl($this, $file, ArrayHelper::getValue($options, 'appendTimestamp')), $options); + } elseif ($css !== null) { + $view->registerCssFile($manager->getAssetUrl($this, $css), $this->cssOptions); } } } diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 2aa8c25..ebaeffc 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -34,7 +34,7 @@ use yii\helpers\Url; * For more details and usage information on AssetManager, see the [guide article on assets](guide:structure-assets). * * @property AssetConverterInterface $converter The asset converter. Note that the type of this property - * differs in getter and setter. See [[getConverter()]] and [[setConverter()]] for details. + * differs in getter and setter. See [[getConverter()]] and [[setConverter()]] for details. * * @author Qiang Xue * @since 2.0 @@ -209,12 +209,34 @@ class AssetManager extends Component { parent::init(); $this->basePath = Yii::getAlias($this->basePath); + + $this->basePath = realpath($this->basePath); + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } + + private $_isBasePathPermissionChecked; + + /** + * Check whether the basePath exists and is writeable. + * + * @since 2.0.40 + */ + public function checkBasePathPermission() + { + // if the check is been done already, skip further checks + if ($this->_isBasePathPermissionChecked) { + return; + } + if (!is_dir($this->basePath)) { throw new InvalidConfigException("The directory does not exist: {$this->basePath}"); } - $this->basePath = realpath($this->basePath); - $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + if (!is_writable($this->basePath)) { + throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}"); + } + + $this->_isBasePathPermissionChecked = true; } /** @@ -278,12 +300,12 @@ class AssetManager extends Component protected function loadDummyBundle($name) { if (!isset($this->_dummyBundles[$name])) { - $this->_dummyBundles[$name] = $this->loadBundle($name, [ - 'sourcePath' => null, - 'js' => [], - 'css' => [], - 'depends' => [], - ]); + $bundle = Yii::createObject(['class' => $name]); + $bundle->sourcePath = null; + $bundle->js = []; + $bundle->css = []; + + $this->_dummyBundles[$name] = $bundle; } return $this->_dummyBundles[$name]; @@ -294,34 +316,24 @@ class AssetManager extends Component * The actual URL is obtained by prepending either [[AssetBundle::$baseUrl]] or [[AssetManager::$baseUrl]] to the given asset path. * @param AssetBundle $bundle the asset bundle which the asset file belongs to * @param string $asset the asset path. This should be one of the assets listed in [[AssetBundle::$js]] or [[AssetBundle::$css]]. + * @param bool|null $appendTimestamp Whether to append timestamp to the URL. * @return string the actual URL for the specified asset. */ - public function getAssetUrl($bundle, $asset) + public function getAssetUrl($bundle, $asset, $appendTimestamp = null) { - if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) { - if (strncmp($actualAsset, '@web/', 5) === 0) { - $asset = substr($actualAsset, 5); - $basePath = Yii::getAlias('@webroot'); - $baseUrl = Yii::getAlias('@web'); - } else { - $asset = Yii::getAlias($actualAsset); - $basePath = $this->basePath; - $baseUrl = $this->baseUrl; - } - } else { - $basePath = $bundle->basePath; - $baseUrl = $bundle->baseUrl; - } + $assetUrl = $this->getActualAssetUrl($bundle, $asset); + $assetPath = $this->getAssetPath($bundle, $asset); - if (!Url::isRelative($asset) || strncmp($asset, '/', 1) === 0) { - return $asset; + $withTimestamp = $this->appendTimestamp; + if ($appendTimestamp !== null) { + $withTimestamp = $appendTimestamp; } - if ($this->appendTimestamp && ($timestamp = @filemtime("$basePath/$asset")) > 0) { - return "$baseUrl/$asset?v=$timestamp"; + if ($withTimestamp && $assetPath && ($timestamp = @filemtime($assetPath)) > 0) { + return "$assetUrl?v=$timestamp"; } - return "$baseUrl/$asset"; + return $assetUrl; } /** @@ -455,10 +467,6 @@ class AssetManager extends Component throw new InvalidArgumentException("The file or directory to be published does not exist: $path"); } - if (!is_writable($this->basePath)) { - throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}"); - } - if (is_file($src)) { return $this->_published[$path] = $this->publishFile($src); } @@ -474,6 +482,8 @@ class AssetManager extends Component */ protected function publishFile($src) { + $this->checkBasePathPermission(); + $dir = $this->hash($src); $fileName = basename($src); $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; @@ -529,6 +539,8 @@ class AssetManager extends Component */ protected function publishDirectory($src, $options) { + $this->checkBasePathPermission(); + $dir = $this->hash($src); $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; if ($this->linkAssets) { @@ -625,4 +637,33 @@ class AssetManager extends Component $path = (is_file($path) ? dirname($path) : $path) . filemtime($path); return sprintf('%x', crc32($path . Yii::getVersion() . '|' . $this->linkAssets)); } + + /** + * Returns the actual URL for the specified asset. Without parameters. + * The actual URL is obtained by prepending either [[AssetBundle::$baseUrl]] or [[AssetManager::$baseUrl]] to the given asset path. + * @param AssetBundle $bundle the asset bundle which the asset file belongs to + * @param string $asset the asset path. This should be one of the assets listed in [[AssetBundle::$js]] or [[AssetBundle::$css]]. + * @return string the actual URL for the specified asset. + * @since 2.0.39 + */ + public function getActualAssetUrl($bundle, $asset) + { + if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) { + if (strncmp($actualAsset, '@web/', 5) === 0) { + $asset = substr($actualAsset, 5); + $baseUrl = Yii::getAlias('@web'); + } else { + $asset = Yii::getAlias($actualAsset); + $baseUrl = $this->baseUrl; + } + } else { + $baseUrl = $bundle->baseUrl; + } + + if (!Url::isRelative($asset) || strncmp($asset, '/', 1) === 0) { + return $asset; + } + + return "$baseUrl/$asset"; + } } diff --git a/framework/web/CacheSession.php b/framework/web/CacheSession.php index 9185359..ecb14b7 100644 --- a/framework/web/CacheSession.php +++ b/framework/web/CacheSession.php @@ -31,7 +31,7 @@ use yii\di\Instance; * ] * ``` * - * @property bool $useCustomStorage Whether to use custom storage. This property is read-only. + * @property-read bool $useCustomStorage Whether to use custom storage. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -70,6 +70,26 @@ class CacheSession extends Session } /** + * Session open handler. + * @internal Do not call this method directly. + * @param string $savePath session save path + * @param string $sessionName session name + * @return bool whether session is opened successfully + */ + public function openSession($savePath, $sessionName) + { + if ($this->getUseStrictMode()) { + $id = $this->getId(); + if (!$this->cache->exists($this->calculateKey($id))) { + //This session id does not exist, mark it for forced regeneration + $this->_forceRegenerateId = $id; + } + } + + return parent::openSession($savePath, $sessionName); + } + + /** * Session read handler. * @internal Do not call this method directly. * @param string $id session ID @@ -91,6 +111,11 @@ class CacheSession extends Session */ public function writeSession($id, $data) { + if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) { + //Ignore write when forceRegenerate is active for this id + return true; + } + return $this->cache->set($this->calculateKey($id), $data, $this->getTimeout()); } diff --git a/framework/web/CompositeUrlRule.php b/framework/web/CompositeUrlRule.php index d985f07..d707c64 100644 --- a/framework/web/CompositeUrlRule.php +++ b/framework/web/CompositeUrlRule.php @@ -13,8 +13,8 @@ use yii\base\BaseObject; /** * CompositeUrlRule is the base class for URL rule classes that consist of multiple simpler rules. * - * @property null|int $createUrlStatus Status of the URL creation after the last [[createUrl()]] call. `null` - * if rule does not provide info about create status. This property is read-only. + * @property-read null|int $createUrlStatus Status of the URL creation after the last [[createUrl()]] call. + * `null` if rule does not provide info about create status. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/web/Controller.php b/framework/web/Controller.php index f3f5380..08a15e8 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -8,6 +8,7 @@ namespace yii\web; use Yii; +use yii\base\Exception; use yii\base\InlineAction; use yii\helpers\Url; @@ -16,6 +17,9 @@ use yii\helpers\Url; * * For more details and usage information on Controller, see the [guide article on controllers](guide:structure-controllers). * + * @property Request $request + * @property Response $response + * * @author Qiang Xue * @since 2.0 */ @@ -70,10 +74,9 @@ class Controller extends \yii\base\Controller */ public function asJson($data) { - $response = Yii::$app->getResponse(); - $response->format = Response::FORMAT_JSON; - $response->data = $data; - return $response; + $this->response->format = Response::FORMAT_JSON; + $this->response->data = $data; + return $this->response; } /** @@ -97,10 +100,9 @@ class Controller extends \yii\base\Controller */ public function asXml($data) { - $response = Yii::$app->getResponse(); - $response->format = Response::FORMAT_XML; - $response->data = $data; - return $response; + $this->response->format = Response::FORMAT_XML; + $this->response->data = $data; + return $this->response; } /** @@ -125,19 +127,62 @@ class Controller extends \yii\base\Controller $args = []; $missing = []; $actionParams = []; + $requestedParams = []; foreach ($method->getParameters() as $param) { $name = $param->getName(); if (array_key_exists($name, $params)) { - if ($param->isArray()) { - $args[] = $actionParams[$name] = (array) $params[$name]; - } elseif (!is_array($params[$name])) { - $args[] = $actionParams[$name] = $params[$name]; + $isValid = true; + if (PHP_VERSION_ID >= 80000) { + $isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array'; } else { - throw new BadRequestHttpException(Yii::t('yii', 'Invalid data received for parameter "{param}".', [ - 'param' => $name, - ])); + $isArray = $param->isArray(); + } + if ($isArray) { + $params[$name] = (array)$params[$name]; + } elseif (is_array($params[$name])) { + $isValid = false; + } elseif ( + PHP_VERSION_ID >= 70000 + && ($type = $param->getType()) !== null + && $type->isBuiltin() + && ($params[$name] !== null || !$type->allowsNull()) + ) { + $typeName = PHP_VERSION_ID >= 70100 ? $type->getName() : (string)$type; + + if ($params[$name] === '' && $type->allowsNull()) { + if ($typeName !== 'string') { // for old string behavior compatibility + $params[$name] = null; + } + } else { + switch ($typeName) { + case 'int': + $params[$name] = filter_var($params[$name], FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); + break; + case 'float': + $params[$name] = filter_var($params[$name], FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE); + break; + case 'bool': + $params[$name] = filter_var($params[$name], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + break; + } + if ($params[$name] === null) { + $isValid = false; + } + } + } + if (!$isValid) { + throw new BadRequestHttpException( + Yii::t('yii', 'Invalid data received for parameter "{param}".', ['param' => $name]) + ); } + $args[] = $actionParams[$name] = $params[$name]; unset($params[$name]); + } elseif (PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null && !$type->isBuiltin()) { + try { + $this->bindInjectedParams($type, $name, $args, $requestedParams); + } catch (Exception $e) { + throw new ServerErrorHttpException($e->getMessage(), 0, $e); + } } elseif ($param->isDefaultValueAvailable()) { $args[] = $actionParams[$name] = $param->getDefaultValue(); } else { @@ -146,13 +191,18 @@ class Controller extends \yii\base\Controller } if (!empty($missing)) { - throw new BadRequestHttpException(Yii::t('yii', 'Missing required parameters: {params}', [ - 'params' => implode(', ', $missing), - ])); + throw new BadRequestHttpException( + Yii::t('yii', 'Missing required parameters: {params}', ['params' => implode(', ', $missing)]) + ); } $this->actionParams = $actionParams; + // We use a different array here, specifically one that doesn't contain service instances but descriptions instead. + if (Yii::$app->requestedParams === null) { + Yii::$app->requestedParams = array_merge($actionParams, $requestedParams); + } + return $args; } @@ -162,7 +212,7 @@ class Controller extends \yii\base\Controller public function beforeAction($action) { if (parent::beforeAction($action)) { - if ($this->enableCsrfValidation && Yii::$app->getErrorHandler()->exception === null && !Yii::$app->getRequest()->validateCsrfToken()) { + if ($this->enableCsrfValidation && Yii::$app->getErrorHandler()->exception === null && !$this->request->validateCsrfToken()) { throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.')); } @@ -201,7 +251,7 @@ class Controller extends \yii\base\Controller public function redirect($url, $statusCode = 302) { // calling Url::to() here because Response::redirect() modifies route before calling Url::to() - return Yii::$app->getResponse()->redirect(Url::to($url), $statusCode); + return $this->response->redirect(Url::to($url), $statusCode); } /** @@ -218,7 +268,7 @@ class Controller extends \yii\base\Controller */ public function goHome() { - return Yii::$app->getResponse()->redirect(Yii::$app->getHomeUrl()); + return $this->response->redirect(Yii::$app->getHomeUrl()); } /** @@ -241,7 +291,7 @@ class Controller extends \yii\base\Controller */ public function goBack($defaultUrl = null) { - return Yii::$app->getResponse()->redirect(Yii::$app->getUser()->getReturnUrl($defaultUrl)); + return $this->response->redirect(Yii::$app->getUser()->getReturnUrl($defaultUrl)); } /** @@ -261,6 +311,6 @@ class Controller extends \yii\base\Controller */ public function refresh($anchor = '') { - return Yii::$app->getResponse()->redirect(Yii::$app->getRequest()->getUrl() . $anchor); + return $this->response->redirect($this->request->getUrl() . $anchor); } } diff --git a/framework/web/Cookie.php b/framework/web/Cookie.php index f517ca3..7ba7d02 100644 --- a/framework/web/Cookie.php +++ b/framework/web/Cookie.php @@ -67,17 +67,12 @@ class Cookie extends \yii\base\BaseObject public $httpOnly = true; /** * @var string SameSite prevents the browser from sending this cookie along with cross-site requests. - * Please note that this feature is only supported since PHP 7.3.0 - * For better security, an exception will be thrown if `sameSite` is set while using an unsupported version of PHP. - * To use this feature across different PHP versions check the version first. E.g. - * ```php - * $cookie->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. * * @since 2.0.21 */ - public $sameSite; + public $sameSite = self::SAME_SITE_LAX; /** diff --git a/framework/web/CookieCollection.php b/framework/web/CookieCollection.php index e5b2b68..c26cfd3 100644 --- a/framework/web/CookieCollection.php +++ b/framework/web/CookieCollection.php @@ -17,9 +17,9 @@ use yii\base\InvalidCallException; * * For more details and usage information on CookieCollection, see the [guide article on handling cookies](guide:runtime-sessions-cookies). * - * @property int $count The number of cookies in the collection. This property is read-only. - * @property ArrayIterator $iterator An iterator for traversing the cookies in the collection. This property - * is read-only. + * @property-read int $count The number of cookies in the collection. This property is read-only. + * @property-read ArrayIterator $iterator An iterator for traversing the cookies in the collection. This + * property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index dfb825f..bc23f66 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -13,7 +13,6 @@ use yii\db\Connection; use yii\db\PdoValue; use yii\db\Query; use yii\di\Instance; -use yii\helpers\ArrayHelper; /** * DbSession extends [[Session]] by using database as session data storage. @@ -95,9 +94,27 @@ class DbSession extends MultiFieldSession } /** - * Updates the current session ID with a newly generated one . - * Please refer to for more details. - * @param bool $deleteOldSession Whether to delete the old associated session file or not. + * Session open handler. + * @internal Do not call this method directly. + * @param string $savePath session save path + * @param string $sessionName session name + * @return bool whether session is opened successfully + */ + public function openSession($savePath, $sessionName) + { + if ($this->getUseStrictMode()) { + $id = $this->getId(); + if (!$this->getReadQuery($id)->exists()) { + //This session id does not exist, mark it for forced regeneration + $this->_forceRegenerateId = $id; + } + } + + return parent::openSession($savePath, $sessionName); + } + + /** + * {@inheritdoc} */ public function regenerateID($deleteOldSession = false) { @@ -163,9 +180,7 @@ class DbSession extends MultiFieldSession */ public function readSession($id) { - $query = new Query(); - $query->from($this->sessionTable) - ->where('[[expire]]>:expire AND [[id]]=:id', [':expire' => time(), ':id' => $id]); + $query = $this->getReadQuery($id); if ($this->readCallback !== null) { $fields = $query->one($this->db); @@ -185,6 +200,11 @@ class DbSession extends MultiFieldSession */ public function writeSession($id, $data) { + if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) { + //Ignore write when forceRegenerate is active for this id + return true; + } + // exception must be caught in session write handler // https://secure.php.net/manual/en/function.session-set-save-handler.php#refsect1-function.session-set-save-handler-notes try { @@ -244,6 +264,18 @@ class DbSession extends MultiFieldSession } /** + * Generates a query to get the session from db + * @param string $id The id of the session + * @return Query + */ + protected function getReadQuery($id) + { + return (new Query()) + ->from($this->sessionTable) + ->where('[[expire]]>:expire AND [[id]]=:id', [':expire' => time(), ':id' => $id]); + } + + /** * Method typecasts $fields before passing them to PDO. * Default implementation casts field `data` to `\PDO::PARAM_LOB`. * You can override this method in case you need special type casting. diff --git a/framework/web/ErrorHandler.php b/framework/web/ErrorHandler.php index 52736c2..8c9ee27 100644 --- a/framework/web/ErrorHandler.php +++ b/framework/web/ErrorHandler.php @@ -105,6 +105,7 @@ class ErrorHandler extends \yii\base\ErrorHandler $useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException); if ($useErrorView && $this->errorAction !== null) { + Yii::$app->view->clear(); $result = Yii::$app->runAction($this->errorAction); if ($result instanceof Response) { $response = $result; @@ -482,7 +483,7 @@ class ErrorHandler extends \yii\base\ErrorHandler /** * Returns human-readable exception name. * @param \Exception $exception - * @return string human-readable exception name or null if it cannot be determined + * @return string|null human-readable exception name or null if it cannot be determined */ public function getExceptionName($exception) { diff --git a/framework/web/HeaderCollection.php b/framework/web/HeaderCollection.php index 7c135d2..1fa6021 100644 --- a/framework/web/HeaderCollection.php +++ b/framework/web/HeaderCollection.php @@ -13,9 +13,9 @@ use yii\base\BaseObject; /** * HeaderCollection is used by [[Response]] to maintain the currently registered HTTP headers. * - * @property int $count The number of headers in the collection. This property is read-only. - * @property \ArrayIterator $iterator An iterator for traversing the headers in the collection. This property - * is read-only. + * @property-read int $count The number of headers in the collection. This property is read-only. + * @property-read \ArrayIterator $iterator An iterator for traversing the headers in the collection. This + * property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/web/MultiFieldSession.php b/framework/web/MultiFieldSession.php index cd26d12..1b73e6d 100644 --- a/framework/web/MultiFieldSession.php +++ b/framework/web/MultiFieldSession.php @@ -22,7 +22,7 @@ namespace yii\web; * While extending this class you should use [[composeFields()]] method - while writing the session data into the storage and * [[extractData()]] - while reading session data from the storage. * - * @property bool $useCustomStorage Whether to use custom storage. This property is read-only. + * @property-read bool $useCustomStorage Whether to use custom storage. This property is read-only. * * @author Paul Klimov * @since 2.0.6 diff --git a/framework/web/MultipartFormDataParser.php b/framework/web/MultipartFormDataParser.php index c9b7bd0..c6f8c8c 100644 --- a/framework/web/MultipartFormDataParser.php +++ b/framework/web/MultipartFormDataParser.php @@ -54,8 +54,7 @@ use yii\helpers\StringHelper; * * > Note: although this parser fully emulates regular structure of the `$_FILES`, related temporary * files, which are available via `tmp_name` key, will not be recognized by PHP as uploaded ones. - * Thus functions like `is_uploaded_file()` and `move_uploaded_file()` will fail on them. This also - * means [[UploadedFile::saveAs()]] will fail as well. + * Thus functions like `is_uploaded_file()` and `move_uploaded_file()` will fail on them. * * @property int $uploadFileMaxCount Maximum upload files count. * @property int $uploadFileMaxSize Upload file max size in bytes. @@ -142,10 +141,11 @@ class MultipartFormDataParser extends BaseObject implements RequestParserInterfa return []; } - if (!preg_match('/boundary=(.*)$/is', $contentType, $matches)) { + if (!preg_match('/boundary="?(.*)"?$/is', $contentType, $matches)) { return []; } - $boundary = $matches[1]; + + $boundary = trim($matches[1], '"'); $bodyParts = preg_split('/\\R?-+' . preg_quote($boundary, '/') . '/s', $rawBody); array_pop($bodyParts); // last block always has no data, contains boundary ending like `--` @@ -191,6 +191,7 @@ class MultipartFormDataParser extends BaseObject implements RequestParserInterfa @fclose($tmpResource); } else { fwrite($tmpResource, $value); + rewind($tmpResource); $fileInfo['tmp_name'] = $tmpFileName; $fileInfo['tmp_resource'] = $tmpResource; // save file resource, otherwise it will be deleted } @@ -217,7 +218,7 @@ class MultipartFormDataParser extends BaseObject implements RequestParserInterfa private function parseHeaders($headerContent) { $headers = []; - $headerParts = preg_split('/\\R/s', $headerContent, -1, PREG_SPLIT_NO_EMPTY); + $headerParts = preg_split('/\\R/su', $headerContent, -1, PREG_SPLIT_NO_EMPTY); foreach ($headerParts as $headerPart) { if (strpos($headerPart, ':') === false) { continue; diff --git a/framework/web/Request.php b/framework/web/Request.php index 7dcc4c7..70fa32d 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -23,70 +23,73 @@ use yii\validators\IpValidator; * * For more details and usage information on Request, see the [guide article on requests](guide:runtime-requests). * - * @property string $absoluteUrl The currently requested absolute URL. This property is read-only. + * @property-read string $absoluteUrl The currently requested absolute URL. This property is read-only. * @property array $acceptableContentTypes The content types ordered by the quality score. Types with the * highest scores will be returned first. The array keys are the content types, while the array values are the * corresponding quality score and other parameters as given in the header. * @property array $acceptableLanguages The languages ordered by the preference level. The first element * represents the most preferred language. - * @property array $authCredentials That contains exactly two elements: - 0: the username sent via HTTP + * @property-read array $authCredentials That contains exactly two elements: - 0: the username sent via HTTP * authentication, `null` if the username is not given - 1: the password sent via HTTP authentication, `null` if * the password is not given. This property is read-only. - * @property string|null $authPassword The password sent via HTTP authentication, `null` if the password is + * @property-read string|null $authPassword The password sent via HTTP authentication, `null` if the password + * is not given. This property is read-only. + * @property-read string|null $authUser The username sent via HTTP authentication, `null` if the username is * not given. This property is read-only. - * @property string|null $authUser The username sent via HTTP authentication, `null` if the username is not - * given. This property is read-only. * @property string $baseUrl The relative URL for the application. * @property array $bodyParams The request parameters given in the request body. - * @property string $contentType Request content-type. Null is returned if this information is not available. - * This property is read-only. - * @property CookieCollection $cookies The cookie collection. This property is read-only. - * @property string $csrfToken The token used to perform CSRF validation. This property is read-only. - * @property string $csrfTokenFromHeader The CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned - * if no such header is sent. This property is read-only. - * @property array $eTags The entity tags. This property is read-only. - * @property HeaderCollection $headers The header collection. This property is read-only. + * @property-read string $contentType Request content-type. Null is returned if this information is not + * available. This property is read-only. + * @property-read CookieCollection $cookies The cookie collection. This property is read-only. + * @property-read string $csrfToken The token used to perform CSRF validation. This property is read-only. + * @property-read string $csrfTokenFromHeader The CSRF token sent via [[CSRF_HEADER]] by browser. Null is + * returned if no such header is sent. This property is read-only. + * @property-read array $eTags The entity tags. This property is read-only. + * @property-read HeaderCollection $headers The header collection. This property is read-only. * @property string|null $hostInfo Schema and hostname part (with port number if needed) of the request URL * (e.g. `http://www.yiiframework.com`), null if can't be obtained from `$_SERVER` and wasn't set. See * [[getHostInfo()]] for security related notes on this property. - * @property string|null $hostName Hostname part of the request URL (e.g. `www.yiiframework.com`). This + * @property-read string|null $hostName Hostname part of the request URL (e.g. `www.yiiframework.com`). This * property is read-only. - * @property bool $isAjax Whether this is an AJAX (XMLHttpRequest) request. This property is read-only. - * @property bool $isDelete Whether this is a DELETE request. This property is read-only. - * @property bool $isFlash Whether this is an Adobe Flash or Adobe Flex request. This property is read-only. - * @property bool $isGet Whether this is a GET request. This property is read-only. - * @property bool $isHead Whether this is a HEAD request. This property is read-only. - * @property bool $isOptions Whether this is a OPTIONS request. This property is read-only. - * @property bool $isPatch Whether this is a PATCH request. This property is read-only. - * @property bool $isPjax Whether this is a PJAX request. This property is read-only. - * @property bool $isPost Whether this is a POST request. This property is read-only. - * @property bool $isPut Whether this is a PUT request. This property is read-only. - * @property bool $isSecureConnection If the request is sent via secure channel (https). This property is + * @property-read bool $isAjax Whether this is an AJAX (XMLHttpRequest) request. This property is read-only. + * @property-read bool $isDelete Whether this is a DELETE request. This property is read-only. + * @property-read bool $isFlash Whether this is an Adobe Flash or Adobe Flex request. This property is * read-only. - * @property string $method Request method, such as GET, POST, HEAD, PUT, PATCH, DELETE. The value returned is - * turned into upper case. This property is read-only. - * @property string|null $origin URL origin of a CORS request, `null` if not available. This property is + * @property-read bool $isGet Whether this is a GET request. This property is read-only. + * @property-read bool $isHead Whether this is a HEAD request. This property is read-only. + * @property-read bool $isOptions Whether this is a OPTIONS request. This property is read-only. + * @property-read bool $isPatch Whether this is a PATCH request. This property is read-only. + * @property-read bool $isPjax Whether this is a PJAX request. This property is read-only. + * @property-read bool $isPost Whether this is a POST request. This property is read-only. + * @property-read bool $isPut Whether this is a PUT request. This property is read-only. + * @property-read bool $isSecureConnection If the request is sent via secure channel (https). This property is + * read-only. + * @property-read string $method Request method, such as GET, POST, HEAD, PUT, PATCH, DELETE. The value + * returned is turned into upper case. This property is read-only. + * @property-read string|null $origin URL origin of a CORS request, `null` if not available. This property is * read-only. * @property string $pathInfo Part of the request URL that is after the entry script and before the question * mark. Note, the returned path info is already URL-decoded. * @property int $port Port number for insecure requests. * @property array $queryParams The request GET parameter values. - * @property string $queryString Part of the request URL that is after the question mark. This property is - * read-only. + * @property-read string $queryString Part of the request URL that is after the question mark. This property + * is read-only. * @property string $rawBody The request body. - * @property string|null $referrer URL referrer, null if not available. This property is read-only. - * @property string|null $remoteHost Remote host name, `null` if not available. This property is read-only. - * @property string|null $remoteIP Remote IP address, `null` if not available. This property is read-only. + * @property-read string|null $referrer URL referrer, null if not available. This property is read-only. + * @property-read string|null $remoteHost Remote host name, `null` if not available. This property is + * read-only. + * @property-read string|null $remoteIP Remote IP address, `null` if not available. This property is + * read-only. * @property string $scriptFile The entry script file path. * @property string $scriptUrl The relative URL of the entry script. * @property int $securePort Port number for secure requests. - * @property string $serverName Server name, null if not available. This property is read-only. - * @property int|null $serverPort Server port number, null if not available. This property is read-only. + * @property-read string $serverName Server name, null if not available. This property is read-only. + * @property-read int|null $serverPort Server port number, null if not available. This property is read-only. * @property string $url The currently requested relative URL. Note that the URI returned may be URL-encoded * depending on the client. - * @property string|null $userAgent User agent, null if not available. This property is read-only. - * @property string|null $userHost User host name, null if not available. This property is read-only. - * @property string|null $userIP User IP address, null if not available. This property is read-only. + * @property-read string|null $userAgent User agent, null if not available. This property is read-only. + * @property-read string|null $userHost User host name, null if not available. This property is read-only. + * @property-read string|null $userIP User IP address, null if not available. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -212,8 +215,10 @@ class Request extends \yii\base\Request /** * @var array lists of headers that are, by default, subject to the trusted host configuration. * These headers will be filtered unless explicitly allowed in [[trustedHosts]]. + * If the list contains the `Forwarded` header, processing will be done according to RFC 7239. * The match of header names is case-insensitive. * @see https://en.wikipedia.org/wiki/List_of_HTTP_header_fields + * @see https://tools.ietf.org/html/rfc7239 * @see $trustedHosts * @since 2.0.13 */ @@ -226,10 +231,14 @@ class Request extends \yii\base\Request // Microsoft: 'Front-End-Https', 'X-Rewrite-Url', + + // ngrok: + 'X-Original-Host', ]; /** * @var string[] List of headers where proxies store the real client IP. * It's not advisable to put insecure headers here. + * To use the `Forwarded` header according to RFC 7239, the header must be added to [[secureHeaders]] list. * The match of header names is case-insensitive. * @see $trustedHosts * @see $secureHeaders @@ -291,6 +300,23 @@ class Request extends \yii\base\Request */ protected function filterHeaders(HeaderCollection $headerCollection) { + $trustedHeaders = $this->getTrustedHeaders(); + + // remove all secure headers unless they are trusted + foreach ($this->secureHeaders as $secureHeader) { + if (!in_array($secureHeader, $trustedHeaders)) { + $headerCollection->remove($secureHeader); + } + } + } + + /** + * Trusted headers according to the [[trustedHosts]]. + * @return array + * @since 2.0.28 + */ + protected function getTrustedHeaders() + { // do not trust any of the [[secureHeaders]] by default $trustedHeaders = []; @@ -310,13 +336,7 @@ class Request extends \yii\base\Request } } } - - // filter all secure headers unless they are trusted - foreach ($this->secureHeaders as $secureHeader) { - if (!in_array($secureHeader, $trustedHeaders)) { - $headerCollection->remove($secureHeader); - } - } + return $trustedHeaders; } /** @@ -457,8 +477,17 @@ class Request extends \yii\base\Request /** * Returns whether this is an AJAX (XMLHttpRequest) request. * - * Note that jQuery doesn't set the header in case of cross domain - * requests: https://stackoverflow.com/questions/8163703/cross-domain-ajax-doesnt-send-x-requested-with-header + * Note that in case of cross domain requests, browser doesn't set the X-Requested-With header by default: + * https://stackoverflow.com/questions/8163703/cross-domain-ajax-doesnt-send-x-requested-with-header + * + * In case you are using `fetch()`, pass header manually: + * + * ``` + * fetch(url, { + * method: 'GET', + * headers: {'X-Requested-With': 'XMLHttpRequest'} + * }) + * ``` * * @return bool whether this is an AJAX (XMLHttpRequest) request. */ @@ -714,8 +743,12 @@ class Request extends \yii\base\Request $secure = $this->getIsSecureConnection(); $http = $secure ? 'https' : 'http'; - if ($this->headers->has('X-Forwarded-Host')) { + if ($this->getSecureForwardedHeaderTrustedPart('host') !== null) { + $this->_hostInfo = $http . '://' . $this->getSecureForwardedHeaderTrustedPart('host'); + } elseif ($this->headers->has('X-Forwarded-Host')) { $this->_hostInfo = $http . '://' . trim(explode(',', $this->headers->get('X-Forwarded-Host'))[0]); + } elseif ($this->headers->has('X-Original-Host')) { + $this->_hostInfo = $http . '://' . trim(explode(',', $this->headers->get('X-Original-Host'))[0]); } elseif ($this->headers->has('Host')) { $this->_hostInfo = $http . '://' . $this->headers->get('Host'); } elseif (isset($_SERVER['SERVER_NAME'])) { @@ -1055,6 +1088,11 @@ class Request extends \yii\base\Request if (isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'], 'on') === 0 || $_SERVER['HTTPS'] == 1)) { return true; } + + if (($proto = $this->getSecureForwardedHeaderTrustedPart('proto')) !== null) { + return strcasecmp($proto, 'https') === 0; + } + foreach ($this->secureProtocolHeaders as $header => $values) { if (($headerValue = $this->headers->get($header, null)) !== null) { foreach ($values as $value) { @@ -1125,19 +1163,90 @@ class Request extends \yii\base\Request } /** + * Returns the user IP address from [[ipHeaders]]. + * @return string|null user IP address, null if not available + * @see $ipHeaders + * @since 2.0.28 + */ + protected function getUserIpFromIpHeaders() + { + $ip = $this->getSecureForwardedHeaderTrustedPart('for'); + if ($ip !== null && preg_match( + '/^\[?(?P(?:(?:(?:[0-9a-f]{1,4}:){1,6}(?:[0-9a-f]{1,4})?(?:(?::[0-9a-f]{1,4}){1,6}))|(?:[\d]{1,3}\.){3}[\d]{1,3}))\]?(?::(?P[\d]+))?$/', + $ip, + $matches + )) { + $ip = $this->getUserIpFromIpHeader($matches['ip']); + if ($ip !== null) { + return $ip; + } + } + + + foreach ($this->ipHeaders as $ipHeader) { + if ($this->headers->has($ipHeader)) { + $ip = $this->getUserIpFromIpHeader($this->headers->get($ipHeader)); + if ($ip !== null) { + return $ip; + } + } + } + return null; + } + + /** * Returns the user IP address. * The IP is determined using headers and / or `$_SERVER` variables. * @return string|null user IP address, null if not available */ public function getUserIP() { - foreach ($this->ipHeaders as $ipHeader) { - if ($this->headers->has($ipHeader)) { - return trim(explode(',', $this->headers->get($ipHeader))[0]); + $ip = $this->getUserIpFromIpHeaders(); + return $ip === null ? $this->getRemoteIP() : $ip; + } + + /** + * Return user IP's from IP header. + * + * @param string $ips comma separated IP list + * @return string|null IP as string. Null is returned if IP can not be determined from header. + * @see $getUserHost + * @see $ipHeader + * @see $trustedHeaders + * @since 2.0.28 + */ + protected function getUserIpFromIpHeader($ips) + { + $ips = trim($ips); + if ($ips === '') { + return null; + } + $ips = preg_split('/\s*,\s*/', $ips, -1, PREG_SPLIT_NO_EMPTY); + krsort($ips); + $validator = $this->getIpValidator(); + $resultIp = null; + foreach ($ips as $ip) { + $validator->setRanges('any'); + if (!$validator->validate($ip) /* checking IP format */) { + break; + } + $resultIp = $ip; + $isTrusted = false; + foreach ($this->trustedHosts as $trustedCidr => $trustedCidrOrHeaders) { + if (!is_array($trustedCidrOrHeaders)) { + $trustedCidr = $trustedCidrOrHeaders; + } + $validator->setRanges($trustedCidr); + if ($validator->validate($ip) /* checking trusted range */) { + $isTrusted = true; + break; + } + } + if (!$isTrusted) { + break; } } - - return $this->getRemoteIP(); + return $resultIp; } /** @@ -1147,13 +1256,11 @@ class Request extends \yii\base\Request */ public function getUserHost() { - foreach ($this->ipHeaders as $ipHeader) { - if ($this->headers->has($ipHeader)) { - return gethostbyaddr(trim(explode(',', $this->headers->get($ipHeader))[0])); - } + $userIp = $this->getUserIpFromIpHeaders(); + if($userIp === null) { + return $this->getRemoteHost(); } - - return $this->getRemoteHost(); + return gethostbyaddr($userIp); } /** @@ -1743,4 +1850,99 @@ class Request extends \yii\base\Request return $security->compareString($security->unmaskToken($clientSuppliedToken), $security->unmaskToken($trueToken)); } + + /** + * Gets first `Forwarded` header value for token + * + * @param string $token Header token + * + * @return string|null + * + * @since 2.0.31 + */ + protected function getSecureForwardedHeaderTrustedPart($token) + { + $token = strtolower($token); + + if ($parts = $this->getSecureForwardedHeaderTrustedParts()) { + $lastElement = array_pop($parts); + if ($lastElement && isset($lastElement[$token])) { + return $lastElement[$token]; + } + } + return null; + } + + /** + * Gets only trusted `Forwarded` header parts + * + * @return array + * + * @since 2.0.31 + */ + protected function getSecureForwardedHeaderTrustedParts() + { + $validator = $this->getIpValidator(); + $trustedHosts = []; + foreach ($this->trustedHosts as $trustedCidr => $trustedCidrOrHeaders) { + if (!is_array($trustedCidrOrHeaders)) { + $trustedCidr = $trustedCidrOrHeaders; + } + $trustedHosts[] = $trustedCidr; + } + $validator->setRanges($trustedHosts); + + return array_filter($this->getSecureForwardedHeaderParts(), function ($headerPart) use ($validator) { + return isset($headerPart['for']) ? !$validator->validate($headerPart['for']) : true; + }); + } + + private $_secureForwardedHeaderParts; + + /** + * Returns decoded forwarded header + * + * @return array + * + * @since 2.0.31 + */ + protected function getSecureForwardedHeaderParts() + { + if ($this->_secureForwardedHeaderParts !== null) { + return $this->_secureForwardedHeaderParts; + } + if (count(preg_grep('/^forwarded$/i', $this->secureHeaders)) === 0) { + return $this->_secureForwardedHeaderParts = []; + } + /* + * First header is always correct, because proxy CAN add headers + * after last one is found. + * Keep in mind that it is NOT enforced, therefore we cannot be + * sure, that this is really a first one. + * + * FPM keeps last header sent which is a bug. You need to merge + * headers together on your web server before letting FPM handle it + * @see https://bugs.php.net/bug.php?id=78844 + */ + $forwarded = $this->headers->get('Forwarded', ''); + if ($forwarded === '') { + return $this->_secureForwardedHeaderParts = []; + } + + preg_match_all('/(?:[^",]++|"[^"]++")+/', $forwarded, $forwardedElements); + + foreach ($forwardedElements[0] as $forwardedPairs) { + preg_match_all('/(?P\w+)\s*=\s*(?:(?P[^",;]*[^",;\s])|"(?P[^"]+)")/', $forwardedPairs, + $matches, PREG_SET_ORDER); + $this->_secureForwardedHeaderParts[] = array_reduce($matches, function ($carry, $item) { + $value = $item['value']; + if (isset($item['value2']) && $item['value2'] !== '') { + $value = $item['value2']; + } + $carry[strtolower($item['key'])] = $value; + return $carry; + }, []); + } + return $this->_secureForwardedHeaderParts; + } } diff --git a/framework/web/Response.php b/framework/web/Response.php index fced644..7609fbf 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -37,23 +37,27 @@ use yii\helpers\Url; * * For more details and usage information on Response, see the [guide article on responses](guide:runtime-responses). * - * @property CookieCollection $cookies The cookie collection. This property is read-only. - * @property string $downloadHeaders The attachment file name. This property is write-only. - * @property HeaderCollection $headers The header collection. This property is read-only. - * @property bool $isClientError Whether this response indicates a client error. This property is read-only. - * @property bool $isEmpty Whether this response is empty. This property is read-only. - * @property bool $isForbidden Whether this response indicates the current request is forbidden. This property - * is read-only. - * @property bool $isInformational Whether this response is informational. This property is read-only. - * @property bool $isInvalid Whether this response has a valid [[statusCode]]. This property is read-only. - * @property bool $isNotFound Whether this response indicates the currently requested resource is not found. - * This property is read-only. - * @property bool $isOk Whether this response is OK. This property is read-only. - * @property bool $isRedirection Whether this response is a redirection. This property is read-only. - * @property bool $isServerError Whether this response indicates a server error. This property is read-only. - * @property bool $isSuccessful Whether this response is successful. This property is read-only. + * @property-read CookieCollection $cookies The cookie collection. This property is read-only. + * @property-write string $downloadHeaders The attachment file name. This property is write-only. + * @property-read HeaderCollection $headers The header collection. This property is read-only. + * @property-read bool $isClientError Whether this response indicates a client error. This property is + * read-only. + * @property-read bool $isEmpty Whether this response is empty. This property is read-only. + * @property-read bool $isForbidden Whether this response indicates the current request is forbidden. This + * property is read-only. + * @property-read bool $isInformational Whether this response is informational. This property is read-only. + * @property-read bool $isInvalid Whether this response has a valid [[statusCode]]. This property is + * read-only. + * @property-read bool $isNotFound Whether this response indicates the currently requested resource is not + * found. This property is read-only. + * @property-read bool $isOk Whether this response is OK. This property is read-only. + * @property-read bool $isRedirection Whether this response is a redirection. This property is read-only. + * @property-read bool $isServerError Whether this response indicates a server error. This property is + * read-only. + * @property-read bool $isSuccessful Whether this response is successful. This property is read-only. * @property int $statusCode The HTTP status code to send with the response. - * @property \Exception|\Error $statusCodeByException The exception object. This property is write-only. + * @property-write \Exception|\Error|\Throwable $statusCodeByException The exception object. This property is + * write-only. * * @author Qiang Xue * @author Carsten Brandt @@ -62,15 +66,15 @@ use yii\helpers\Url; class Response extends \yii\base\Response { /** - * @event ResponseEvent an event that is triggered at the beginning of [[send()]]. + * @event \yii\base\Event an event that is triggered at the beginning of [[send()]]. */ const EVENT_BEFORE_SEND = 'beforeSend'; /** - * @event ResponseEvent an event that is triggered at the end of [[send()]]. + * @event \yii\base\Event an event that is triggered at the end of [[send()]]. */ const EVENT_AFTER_SEND = 'afterSend'; /** - * @event ResponseEvent an event that is triggered right after [[prepare()]] is called in [[send()]]. + * @event \yii\base\Event an event that is triggered right after [[prepare()]] is called in [[send()]]. * You may respond to this event to filter the response content before it is sent to the client. */ const EVENT_AFTER_PREPARE = 'afterPrepare'; @@ -134,9 +138,12 @@ class Response extends \yii\base\Response */ public $content; /** - * @var resource|array the stream to be sent. This can be a stream handle or an array of stream handle, - * the begin position and the end position. Note that when this property is set, the [[data]] and [[content]] - * properties will be ignored by [[send()]]. + * @var resource|array|callable the stream to be sent. This can be a stream handle or an array of stream handle, + * the begin position and the end position. Alternatively it can be set to a callable, which returns + * (or [yields](https://www.php.net/manual/en/language.generators.syntax.php)) an array of strings that should + * be echoed and flushed out one by one. + * + * Note that when this property is set, the [[data]] and [[content]] properties will be ignored by [[send()]]. */ public $stream; /** @@ -295,7 +302,7 @@ class Response extends \yii\base\Response /** * Sets the response status code based on the exception. - * @param \Exception|\Error $e the exception object. + * @param \Exception|\Error|\Throwable $e the exception object. * @throws InvalidArgumentException if the status code is invalid. * @return $this the response object itself * @since 2.0.12 @@ -411,8 +418,10 @@ class Response extends \yii\base\Response 'sameSite' => !empty($cookie->sameSite) ? $cookie->sameSite : null, ]); } else { + // Work around for setting sameSite cookie prior PHP 7.3 + // https://stackoverflow.com/questions/39750906/php-setcookie-samesite-strict/46971326#46971326 if (!is_null($cookie->sameSite)) { - throw new InvalidConfigException(get_class($cookie) . '::sameSite is not supported by PHP versions < 7.3.0 (set it to null in this environment)'); + $cookie->path .= '; samesite=' . $cookie->sameSite; } setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); } @@ -430,17 +439,30 @@ class Response extends \yii\base\Response return; } - if (function_exists('set_time_limit')) { - set_time_limit(0); // Reset time limit for big files - } else { + // Try to reset time limit for big files + if (!function_exists('set_time_limit') || !@set_time_limit(0)) { Yii::warning('set_time_limit() is not available', __METHOD__); } + if (is_callable($this->stream)) { + $data = call_user_func($this->stream); + foreach ($data as $datum) { + echo $datum; + flush(); + } + return; + } + $chunkSize = 8 * 1024 * 1024; // 8MB per chunk if (is_array($this->stream)) { list($handle, $begin, $end) = $this->stream; - fseek($handle, $begin); + + // only seek if stream is seekable + if ($this->isSeekable($handle)) { + fseek($handle, $begin); + } + while (!feof($handle) && ($pos = ftell($handle)) <= $end) { if ($pos + $chunkSize > $end) { $chunkSize = $end - $pos + 1; @@ -582,8 +604,12 @@ class Response extends \yii\base\Response if (isset($options['fileSize'])) { $fileSize = $options['fileSize']; } else { - fseek($handle, 0, SEEK_END); - $fileSize = ftell($handle); + if ($this->isSeekable($handle)) { + fseek($handle, 0, SEEK_END); + $fileSize = ftell($handle); + } else { + $fileSize = 0; + } } $range = $this->getHttpRange($fileSize); @@ -870,7 +896,7 @@ class Response extends \yii\base\Response if ($checkAjax) { if ($request->getIsAjax()) { - if (in_array($statusCode, [301, 302]) && preg_match('/Trident.*\brv:11\./' /* IE11 */, $request->userAgent)) { + if (in_array($statusCode, [301, 302]) && preg_match('/Trident\/|MSIE[ ]/', $request->userAgent)) { $statusCode = 200; } if ($request->getIsPjax()) { @@ -1047,10 +1073,14 @@ class Response extends \yii\base\Response * Prepares for sending the response. * The default implementation will convert [[data]] into [[content]] and set headers accordingly. * @throws InvalidConfigException if the formatter for the specified format is invalid or [[format]] is not supported + * + * @see https://tools.ietf.org/html/rfc7231#page-53 + * @see https://tools.ietf.org/html/rfc7232#page-18 */ protected function prepare() { - if ($this->statusCode === 204) { + if (in_array($this->getStatusCode(), [204, 304])) { + // A 204/304 response cannot contain a message body according to rfc7231/rfc7232 $this->content = ''; $this->stream = null; return; @@ -1088,4 +1118,20 @@ class Response extends \yii\base\Response } } } + + /** + * Checks if a stream is seekable + * + * @param $handle + * @return bool + */ + private function isSeekable($handle) + { + if (!is_resource($handle)) { + return true; + } + + $metaData = stream_get_meta_data($handle); + return isset($metaData['seekable']) && $metaData['seekable'] === true; + } } diff --git a/framework/web/Session.php b/framework/web/Session.php index b59c479..03d4ece 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -45,27 +45,28 @@ use yii\base\InvalidConfigException; * * For more details and usage information on Session, see the [guide article on sessions](guide:runtime-sessions-cookies). * - * @property array $allFlashes Flash messages (key => message or key => [message1, message2]). This property - * is read-only. - * @property string $cacheLimiter Current cache limiter. This property is read-only. - * @property array $cookieParams The session cookie parameters. This property is read-only. - * @property int $count The number of session variables. This property is read-only. - * @property string $flash The key identifying the flash message. Note that flash messages and normal session - * variables share the same name space. If you have a normal session variable using the same name, its value will - * be overwritten by this method. This property is write-only. + * @property-read array $allFlashes Flash messages (key => message or key => [message1, message2]). This + * property is read-only. + * @property-read string $cacheLimiter Current cache limiter. This property is read-only. + * @property-read array $cookieParams The session cookie parameters. This property is read-only. + * @property-read int $count The number of session variables. This property is read-only. + * @property-write string $flash The key identifying the flash message. Note that flash messages and normal + * session variables share the same name space. If you have a normal session variable using the same name, its + * value will be overwritten by this method. This property is write-only. * @property float $gCProbability The probability (percentage) that the GC (garbage collection) process is * started on every session initialization. * @property bool $hasSessionId Whether the current request has sent the session ID. * @property string $id The current session ID. - * @property bool $isActive Whether the session has started. This property is read-only. - * @property SessionIterator $iterator An iterator for traversing the session variables. This property is + * @property-read bool $isActive Whether the session has started. This property is read-only. + * @property-read SessionIterator $iterator An iterator for traversing the session variables. This property is * read-only. * @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 * default value is 1440 seconds (or the value of "session.gc_maxlifetime" set in php.ini). * @property bool|null $useCookies The value indicating whether cookies should be used to store session IDs. - * @property bool $useCustomStorage Whether to use custom storage. This property is read-only. + * @property-read bool $useCustomStorage Whether to use custom storage. This property is read-only. + * @property-read bool $useStrictMode Whether strict mode is enabled or not. This property is read-only. * @property bool $useTransparentSessionID Whether transparent sid support is enabled or not, defaults to * false. * @@ -75,6 +76,15 @@ use yii\base\InvalidConfigException; class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Countable { /** + * @var string|null Holds the original session module (before a custom handler is registered) so that it can be + * restored when a Session component without custom handler is used after one that has. + */ + static protected $_originalSessionModule = null; + /** + * Polyfill for ini directive session.use-strict-mode for PHP < 5.5.2. + */ + static private $_useStrictModePolyfill = false; + /** * @var string the name of the session variable that stores the flash message data. */ public $flashParam = '__flash'; @@ -84,6 +94,11 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co public $handler; /** + * @var string|null Holds the session id in case useStrictMode is enabled and the session id needs to be regenerated + */ + protected $_forceRegenerateId = null; + + /** * @var array parameter-value pairs to override default session cookie parameters that are used for session_set_cookie_params() function * Array may have the following possible keys: 'lifetime', 'path', 'domain', 'secure', 'httponly' * @see https://secure.php.net/manual/en/function.session-set-cookie-params.php @@ -136,6 +151,11 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co YII_DEBUG ? session_start() : @session_start(); + if ($this->getUseStrictMode() && $this->_forceRegenerateId) { + $this->regenerateID(); + $this->_forceRegenerateId = null; + } + if ($this->getIsActive()) { Yii::info('Session started', __METHOD__); $this->updateFlashCounters(); @@ -152,6 +172,11 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ protected function registerSessionHandler() { + $sessionModuleName = session_module_name(); + if (static::$_originalSessionModule === null) { + static::$_originalSessionModule = $sessionModuleName; + } + if ($this->handler !== null) { if (!is_object($this->handler)) { $this->handler = Yii::createObject($this->handler); @@ -180,6 +205,12 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co [$this, 'gcSession'] ); } + } elseif ( + $sessionModuleName !== static::$_originalSessionModule + && static::$_originalSessionModule !== null + && static::$_originalSessionModule !== 'user' + ) { + session_module_name(static::$_originalSessionModule); } } @@ -191,6 +222,8 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co if ($this->getIsActive()) { YII_DEBUG ? session_write_close() : @session_write_close(); } + + $this->_forceRegenerateId = null; } /** @@ -398,8 +431,8 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co if (PHP_VERSION_ID >= 70300) { session_set_cookie_params($data); } else { - if (!empty($data['sameSite'])) { - throw new InvalidConfigException('sameSite cookie is not supported by PHP versions < 7.3.0 (set it to null in this environment)'); + if (!empty($data['samesite'])) { + $data['path'] .= '; samesite=' . $data['samesite']; } session_set_cookie_params($data['lifetime'], $data['path'], $data['domain'], $data['secure'], $data['httponly']); } @@ -515,6 +548,43 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co } /** + * @var bool Whether strict mode is enabled or not. + * When `true` this setting prevents the session component to use an uninitialized session ID. + * Note: Enabling `useStrictMode` on PHP < 5.5.2 is only supported with custom storage classes. + * Warning! Although enabling strict mode is mandatory for secure sessions, the default value of 'session.use-strict-mode' is `0`. + * @see https://www.php.net/manual/en/session.configuration.php#ini.session.use-strict-mode + * @since 2.0.38 + */ + public function setUseStrictMode($value) + { + if (PHP_VERSION_ID < 50502) { + if ($this->getUseCustomStorage() || !$value) { + self::$_useStrictModePolyfill = $value; + } else { + throw new InvalidConfigException('Enabling `useStrictMode` on PHP < 5.5.2 is only supported with custom storage classes.'); + } + } else { + $this->freeze(); + ini_set('session.use_strict_mode', $value ? '1' : '0'); + $this->unfreeze(); + } + } + + /** + * @return bool Whether strict mode is enabled or not. + * @see setUseStrictMode() + * @since 2.0.38 + */ + public function getUseStrictMode() + { + if (PHP_VERSION_ID < 50502) { + return self::$_useStrictModePolyfill; + } + + return (bool)ini_get('session.use_strict_mode'); + } + + /** * Session open handler. * This method should be overridden if [[useCustomStorage]] returns true. * @internal Do not call this method directly. diff --git a/framework/web/UploadedFile.php b/framework/web/UploadedFile.php index 49a0ffa..ed8fa11 100644 --- a/framework/web/UploadedFile.php +++ b/framework/web/UploadedFile.php @@ -7,7 +7,9 @@ namespace yii\web; +use Yii; use yii\base\BaseObject; +use yii\helpers\ArrayHelper; use yii\helpers\Html; /** @@ -20,10 +22,10 @@ use yii\helpers\Html; * * For more details and usage information on UploadedFile, see the [guide article on handling uploads](guide:input-file-upload). * - * @property string $baseName Original file base name. This property is read-only. - * @property string $extension File extension. This property is read-only. - * @property bool $hasError Whether there is an error with the uploaded file. Check [[error]] for detailed - * error code information. This property is read-only. + * @property-read string $baseName Original file base name. This property is read-only. + * @property-read string $extension File extension. This property is read-only. + * @property-read bool $hasError Whether there is an error with the uploaded file. Check [[error]] for + * detailed error code information. This property is read-only. * * @author Qiang Xue * @since 2.0 @@ -56,10 +58,25 @@ class UploadedFile extends BaseObject */ public $error; + /** + * @var resource a temporary uploaded stream resource used within PUT and PATCH request. + */ + private $_tempResource; private static $_files; /** + * UploadedFile constructor. + * + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($config = []) + { + $this->_tempResource = ArrayHelper::remove($config, 'tempResource'); + parent::__construct($config); + } + + /** * String output. * This is PHP magic method that returns string representation of an object. * The implementation here returns the uploaded file's name. @@ -149,9 +166,8 @@ class UploadedFile extends BaseObject /** * Saves the uploaded file. - * Note that this method uses php's move_uploaded_file() method. If the target file `$file` - * already exists, it will be overwritten. - * @param string $file the file path used to save the uploaded file + * If the target file `$file` already exists, it will be overwritten. + * @param string $file the file path or a path alias used to save the uploaded file. * @param bool $deleteTempFile whether to delete the temporary file after saving. * If true, you will not be able to save the uploaded file again in the current request. * @return bool true whether the file is saved successfully @@ -159,15 +175,37 @@ class UploadedFile extends BaseObject */ public function saveAs($file, $deleteTempFile = true) { - if ($this->error == UPLOAD_ERR_OK) { - if ($deleteTempFile) { - return move_uploaded_file($this->tempName, $file); - } elseif (is_uploaded_file($this->tempName)) { - return copy($this->tempName, $file); - } + if ($this->hasError) { + return false; + } + + $targetFile = Yii::getAlias($file); + if (is_resource($this->_tempResource)) { + $result = $this->copyTempFile($targetFile); + return $deleteTempFile ? @fclose($this->_tempResource) : (bool) $result; } - return false; + return $deleteTempFile ? move_uploaded_file($this->tempName, $targetFile) : copy($this->tempName, $targetFile); + } + + /** + * Copy temporary file into file specified + * + * @param string $targetFile path of the file to copy to + * @return bool|int the total count of bytes copied, or false on failure + * @since 2.0.32 + */ + protected function copyTempFile($targetFile) + { + $target = fopen($targetFile, 'wb'); + if ($target === false) { + return false; + } + + $result = stream_copy_to_stream($this->_tempResource, $target); + @fclose($target); + + return $result; } /** @@ -207,7 +245,8 @@ class UploadedFile extends BaseObject self::$_files = []; if (isset($_FILES) && is_array($_FILES)) { foreach ($_FILES as $class => $info) { - self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']); + $resource = isset($info['tmp_resource']) ? $info['tmp_resource'] : []; + self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error'], $resource); } } } @@ -224,16 +263,18 @@ class UploadedFile extends BaseObject * @param mixed $sizes file sizes provided by PHP * @param mixed $errors uploading issues provided by PHP */ - private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors) + private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors, $tempResources) { if (is_array($names)) { foreach ($names as $i => $name) { - self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]); + $resource = isset($tempResources[$i]) ? $tempResources[$i] : []; + self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i], $resource); } } elseif ((int) $errors !== UPLOAD_ERR_NO_FILE) { self::$_files[$key] = [ 'name' => $names, 'tempName' => $tempNames, + 'tempResource' => $tempResources, 'type' => $types, 'size' => $sizes, 'error' => $errors, diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 88d6b0e..13e15e4 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -11,6 +11,7 @@ use Yii; use yii\base\Component; use yii\base\InvalidConfigException; use yii\caching\CacheInterface; +use yii\di\Instance; use yii\helpers\Url; /** @@ -118,12 +119,14 @@ class UrlManager extends Component */ public $routeParam = 'r'; /** - * @var CacheInterface|string the cache object or the application component ID of the cache object. + * @var CacheInterface|array|string the cache object or the application component ID of the cache object. + * This can also be an array that is used to create a [[CacheInterface]] instance in case you do not want to use + * an application component. * Compiled URL rules will be cached through this cache object, if it is available. * * After the UrlManager object is created, if you want to change this property, * you should only assign it with a cache object. - * Set this property to `false` if you do not want to cache the URL rules. + * Set this property to `false` or `null` if you do not want to cache the URL rules. * * Cache entries are stored for the time set by [[\yii\caching\Cache::$defaultDuration|$defaultDuration]] in * the cache configuration, which is unlimited by default. You may want to tune this value if your [[rules]] @@ -182,8 +185,12 @@ class UrlManager extends Component if (!$this->enablePrettyUrl) { return; } - if (is_string($this->cache)) { - $this->cache = Yii::$app->get($this->cache, false); + if ($this->cache !== false && $this->cache !== null) { + try { + $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface'); + } catch (InvalidConfigException $e) { + Yii::warning('Unable to use cache for URL manager: ' . $e->getMessage()); + } } if (empty($this->rules)) { return; @@ -238,10 +245,6 @@ class UrlManager extends Component $rule = ['route' => $rule]; if (preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches)) { $rule['verb'] = explode(',', $matches[1]); - // rules that are not applicable for GET requests should not be used to create URLs - if (!in_array('GET', $rule['verb'], true)) { - $rule['mode'] = UrlRule::PARSING_ONLY; - } $key = $matches[4]; } $rule['pattern'] = $key; diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 2f05823..25d3cd8 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -24,8 +24,8 @@ use yii\base\InvalidConfigException; * ] * ``` * - * @property null|int $createUrlStatus Status of the URL creation after the last [[createUrl()]] call. `null` - * if rule does not provide info about create status. This property is read-only. + * @property-read null|int $createUrlStatus Status of the URL creation after the last [[createUrl()]] call. + * `null` if rule does not provide info about create status. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/web/User.php b/framework/web/User.php index 24bb0a4..db1935f 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -11,6 +11,7 @@ use Yii; use yii\base\Component; use yii\base\InvalidConfigException; use yii\base\InvalidValueException; +use yii\di\Instance; use yii\rbac\CheckAccessInterface; /** @@ -46,13 +47,13 @@ use yii\rbac\CheckAccessInterface; * ] * ``` * - * @property string|int $id The unique identifier for the user. If `null`, it means the user is a guest. This - * property is read-only. + * @property-read string|int $id The unique identifier for the user. If `null`, it means the user is a guest. + * This property is read-only. * @property IdentityInterface|null $identity The identity object associated with the currently logged-in * user. `null` is returned if the user is not logged in (not authenticated). - * @property bool $isGuest Whether the current user is a guest. This property is read-only. + * @property-read bool $isGuest Whether the current user is a guest. This property is read-only. * @property string $returnUrl The URL that the user should be redirected to after login. Note that the type - * of this property differs in getter and setter. See [[getReturnUrl()]] and [[setReturnUrl()]] for details. + * of this property differs in getter and setter. See [[getReturnUrl()]] and [[setReturnUrl()]] for details. * * @author Qiang Xue * @since 2.0 @@ -105,7 +106,8 @@ class User extends Component */ public $authTimeout; /** - * @var CheckAccessInterface The access checker to use for checking access. + * @var CheckAccessInterface|string|array The access checker object to use for checking access or the application + * component ID of the access checker. * If not set the application auth manager will be used. * @since 2.0.9 */ @@ -165,8 +167,8 @@ class User extends Component if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); } - if (!empty($this->accessChecker) && is_string($this->accessChecker)) { - $this->accessChecker = Yii::createObject($this->accessChecker); + if ($this->accessChecker !== null) { + $this->accessChecker = Instance::ensure($this->accessChecker, '\yii\rbac\CheckAccessInterface'); } } diff --git a/framework/web/View.php b/framework/web/View.php index 626d7a8..3d2f1e6 100644 --- a/framework/web/View.php +++ b/framework/web/View.php @@ -479,20 +479,27 @@ class View extends \yii\base\View $url = Yii::getAlias($url); $key = $key ?: $url; $depends = ArrayHelper::remove($options, 'depends', []); + $originalOptions = $options; $position = ArrayHelper::remove($options, 'position', self::POS_END); try { - $asssetManagerAppendTimestamp = $this->getAssetManager()->appendTimestamp; + $assetManagerAppendTimestamp = $this->getAssetManager()->appendTimestamp; } catch (InvalidConfigException $e) { $depends = null; // the AssetManager is not available - $asssetManagerAppendTimestamp = false; + $assetManagerAppendTimestamp = false; } - $appendTimestamp = ArrayHelper::remove($options, 'appendTimestamp', $asssetManagerAppendTimestamp); + $appendTimestamp = ArrayHelper::remove($options, 'appendTimestamp', $assetManagerAppendTimestamp); if (empty($depends)) { // register directly without AssetManager - if ($appendTimestamp && Url::isRelative($url) && ($timestamp = @filemtime(Yii::getAlias('@webroot/' . ltrim($url, '/'), false))) > 0) { - $url = $timestamp ? "$url?v=$timestamp" : $url; + if ($appendTimestamp && Url::isRelative($url)) { + $prefix = Yii::getAlias('@web'); + $prefixLength = strlen($prefix); + $trimmedUrl = ltrim((substr($url, 0, $prefixLength) === $prefix) ? substr($url, $prefixLength) : $url, '/'); + $timestamp = @filemtime(Yii::getAlias('@webroot/' . $trimmedUrl, false)); + if ($timestamp > 0) { + $url = $timestamp ? "$url?v=$timestamp" : $url; + } } if ($type === 'js') { $this->jsFiles[$position][$key] = Html::jsFile($url, $options); @@ -504,7 +511,7 @@ class View extends \yii\base\View 'class' => AssetBundle::className(), 'baseUrl' => '', 'basePath' => '@webroot', - (string)$type => [!Url::isRelative($url) ? $url : ltrim($url, '/')], + (string)$type => [ArrayHelper::merge([!Url::isRelative($url) ? $url : ltrim($url, '/')], $originalOptions)], "{$type}Options" => $options, 'depends' => (array)$depends, ]); diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index b9d4a76..4ffe3fc 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -177,6 +177,9 @@ class ActiveField extends Component } catch (\Exception $e) { ErrorHandler::convertExceptionToError($e); return ''; + } catch (\Throwable $e) { + ErrorHandler::convertExceptionToError($e); + return ''; } } @@ -542,6 +545,13 @@ class ActiveField extends Component */ public function radio($options = [], $enclosedByLabel = true) { + if ($this->form->validationStateOn === ActiveForm::VALIDATION_STATE_ON_INPUT) { + $this->addErrorClassIfNeeded($options); + } + + $this->addAriaAttributes($options); + $this->adjustLabelFor($options); + if ($enclosedByLabel) { $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); $this->parts['{label}'] = ''; @@ -557,13 +567,6 @@ class ActiveField extends Component $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); } - if ($this->form->validationStateOn === ActiveForm::VALIDATION_STATE_ON_INPUT) { - $this->addErrorClassIfNeeded($options); - } - - $this->addAriaAttributes($options); - $this->adjustLabelFor($options); - return $this; } @@ -594,6 +597,13 @@ class ActiveField extends Component */ public function checkbox($options = [], $enclosedByLabel = true) { + if ($this->form->validationStateOn === ActiveForm::VALIDATION_STATE_ON_INPUT) { + $this->addErrorClassIfNeeded($options); + } + + $this->addAriaAttributes($options); + $this->adjustLabelFor($options); + if ($enclosedByLabel) { $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); $this->parts['{label}'] = ''; @@ -609,13 +619,6 @@ class ActiveField extends Component $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); } - if ($this->form->validationStateOn === ActiveForm::VALIDATION_STATE_ON_INPUT) { - $this->addErrorClassIfNeeded($options); - } - - $this->addAriaAttributes($options); - $this->adjustLabelFor($options); - return $this; } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index d9bed1f..e30e997 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -18,8 +18,8 @@ use yii\di\Instance; /** * FragmentCache is used by [[\yii\base\View]] to provide caching of page fragments. * - * @property string|false $cachedContent The cached content. False is returned if valid content is not found - * in the cache. This property is read-only. + * @property-read string|false $cachedContent The cached content. False is returned if valid content is not + * found in the cache. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/framework/widgets/Menu.php b/framework/widgets/Menu.php index 6efaa7e..d9775d6 100644 --- a/framework/widgets/Menu.php +++ b/framework/widgets/Menu.php @@ -306,7 +306,7 @@ class Menu extends Widget { if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) { $route = Yii::getAlias($item['url'][0]); - if ($route[0] !== '/' && Yii::$app->controller) { + if (strpos($route, '/') !== 0 && Yii::$app->controller) { $route = Yii::$app->controller->module->getUniqueId() . '/' . $route; } if (ltrim($route, '/') !== $this->route) { diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index 2c9fcc8..1e5d901 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -204,7 +204,7 @@ class Pjax extends Widget if ($this->formSelector !== false) { $formSelector = Json::htmlEncode($this->formSelector !== null ? $this->formSelector : '#' . $id . ' form[data-pjax]'); $submitEvent = Json::htmlEncode($this->submitEvent); - $js .= "\njQuery(document).on($submitEvent, $formSelector, function (event) {jQuery.pjax.submit(event, $options);});"; + $js .= "\njQuery(document).off($submitEvent, $formSelector).on($submitEvent, $formSelector, function (event) {jQuery.pjax.submit(event, $options);});"; } $view = $this->getView(); PjaxAsset::register($view); diff --git a/package.json b/package.json index 8596f2e..e102ef1 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "sinon": "^1.17.6" }, "scripts": { - "test": "./node_modules/.bin/mocha tests/js/tests/*.test.js --timeout 0" + "test": "./node_modules/.bin/mocha tests/js/tests/*.test.js --timeout 0 --colors" }, "repository": { "type": "git", diff --git a/tests/README.md b/tests/README.md index cc72b0e..d4bbe4e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -29,7 +29,7 @@ phpunit --group=mysql,base,i18n You can get a list of available groups via `phpunit --list-groups`. -A single test class could be run like the follwing: +A single test class could be run like the following: ``` phpunit tests/framework/base/ObjectTest.php diff --git a/tests/data/ar/Customer.php b/tests/data/ar/Customer.php index c77683f..e22cce5 100644 --- a/tests/data/ar/Customer.php +++ b/tests/data/ar/Customer.php @@ -47,7 +47,7 @@ class Customer extends ActiveRecord public function getOrders() { - return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('id'); + return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('[[id]]'); } public function getExpensiveOrders() diff --git a/tests/data/ar/DefaultMultiplePk.php b/tests/data/ar/DefaultMultiplePk.php new file mode 100644 index 0000000..de936ae --- /dev/null +++ b/tests/data/ar/DefaultMultiplePk.php @@ -0,0 +1,24 @@ + + * @property int $id + * @property string $second_key_column + * @property string $type + */ +class DefaultMultiplePk extends ActiveRecord +{ + public static function tableName() + { + return 'default_multiple_pk'; + } +} diff --git a/tests/data/ar/Order.php b/tests/data/ar/Order.php index da1f996..475ff1a 100644 --- a/tests/data/ar/Order.php +++ b/tests/data/ar/Order.php @@ -236,4 +236,9 @@ class Order extends ActiveRecord 0 => 'customer_id', ]; } + + public function getQuantityOrderItems() + { + return $this->hasMany(OrderItem::className(), ['order_id' => 'id', 'quantity' => 'id']); + } } diff --git a/tests/data/ar/OrderItem.php b/tests/data/ar/OrderItem.php index 36fab6c..58623fc 100644 --- a/tests/data/ar/OrderItem.php +++ b/tests/data/ar/OrderItem.php @@ -7,6 +7,8 @@ namespace yiiunit\data\ar; +use yii\behaviors\AttributeTypecastBehavior; + /** * Class OrderItem. * @@ -24,6 +26,22 @@ class OrderItem extends ActiveRecord return static::$tableName ?: 'order_item'; } + public function behaviors() + { + return [ + 'typecast' => [ + 'class' => AttributeTypecastBehavior::className(), + 'attributeTypes' => [ + 'order_id' => AttributeTypecastBehavior::TYPE_STRING, + ], + 'typecastAfterValidate' => false, + 'typecastAfterFind' => true, + 'typecastAfterSave' => false, + 'typecastBeforeSave' => false, + ], + ]; + } + public function getOrder() { return $this->hasOne(Order::className(), ['id' => 'order_id']); diff --git a/tests/data/ar/TestTrigger.php b/tests/data/ar/TestTrigger.php new file mode 100644 index 0000000..1340744 --- /dev/null +++ b/tests/data/ar/TestTrigger.php @@ -0,0 +1,22 @@ + 'Lennon'], [['lastName'], 'required'], + [['lastName'], 'string', 'max' => 25], [['underscore_style'], 'yii\captcha\CaptchaValidator'], [['test'], 'required', 'when' => function ($model) { return $model->firstName === 'cebe'; }], ]; diff --git a/tests/data/base/Speaker.php b/tests/data/base/Speaker.php index e6be70c..60d3df3 100644 --- a/tests/data/base/Speaker.php +++ b/tests/data/base/Speaker.php @@ -49,4 +49,17 @@ class Speaker extends Model 'duplicates' => ['firstName', 'firstName', '!underscore_style', '!underscore_style'], ]; } + + private $_checkedValues = []; + + public function customValidatingMethod($attribute, $params, $validator, $current) + { + $this->_checkedValues[] = $current; + $this->addError($attribute, 'Custom method error'); + } + + public function getCheckedValues() + { + return $this->_checkedValues; + } } diff --git a/tests/data/config.php b/tests/data/config.php index 5feb3e8..33897f2 100644 --- a/tests/data/config.php +++ b/tests/data/config.php @@ -37,7 +37,7 @@ $config = [ 'fixture' => __DIR__ . '/sqlite.sql', ], 'sqlsrv' => [ - 'dsn' => 'sqlsrv:Server=localhost,1433;Database=yiitest', + 'dsn' => 'sqlsrv:Server=127.0.0.1,1433;Database=yiitest', 'username' => 'SA', 'password' => 'YourStrong!Passw0rd', 'fixture' => __DIR__ . '/mssql.sql', @@ -49,9 +49,9 @@ $config = [ 'fixture' => __DIR__ . '/postgres.sql', ], 'oci' => [ - 'dsn' => 'oci:dbname=LOCAL_XE;charset=AL32UTF8;', - 'username' => '', - 'password' => '', + 'dsn' => 'oci:dbname=localhost/XE;charset=AL32UTF8;', + 'username' => 'system', + 'password' => 'oracle', 'fixture' => __DIR__ . '/oci.sql', ], ], diff --git a/tests/data/console/migrate_create/create_fields_with_col_method_after_default_value.php b/tests/data/console/migrate_create/create_fields_with_col_method_after_default_value.php new file mode 100644 index 0000000..8d8a579 --- /dev/null +++ b/tests/data/console/migrate_create/create_fields_with_col_method_after_default_value.php @@ -0,0 +1,42 @@ +createTable('{{%test}}', [ + 'id' => \$this->primaryKey(), + 'title' => \$this->string(10)->notNull()->unique()->defaultValue("test")->after("id"), + 'body' => \$this->text()->notNull()->defaultValue("test")->after("title"), + 'address' => \$this->text()->notNull()->defaultValue("test")->after("body"), + 'address2' => \$this->text()->notNull()->defaultValue('te:st')->after("address"), + 'address3' => \$this->text()->notNull()->defaultValue(':te:st:')->after("address2"), + ]); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + \$this->dropTable('{{%test}}'); + } +} + +CODE; diff --git a/tests/data/controllers/TestController.php b/tests/data/controllers/TestController.php new file mode 100644 index 0000000..bbaeefd --- /dev/null +++ b/tests/data/controllers/TestController.php @@ -0,0 +1,28 @@ +actionConfig = $config; + } + + public function actions() + { + return [ + 'error' => array_merge([ + 'class' => ErrorAction::className(), + 'view' => '@yiiunit/data/views/error.php', + ], $this->actionConfig), + ]; + } +} diff --git a/tests/data/mssql.sql b/tests/data/mssql.sql index 7207c18..57f4e59 100644 --- a/tests/data/mssql.sql +++ b/tests/data/mssql.sql @@ -9,6 +9,8 @@ IF OBJECT_ID('[dbo].[customer]', 'U') IS NOT NULL DROP TABLE [dbo].[customer]; IF OBJECT_ID('[dbo].[profile]', 'U') IS NOT NULL DROP TABLE [dbo].[profile]; IF OBJECT_ID('[dbo].[type]', 'U') IS NOT NULL DROP TABLE [dbo].[type]; IF OBJECT_ID('[dbo].[null_values]', 'U') IS NOT NULL DROP TABLE [dbo].[null_values]; +IF OBJECT_ID('[dbo].[test_trigger]', 'U') IS NOT NULL DROP TABLE [dbo].[test_trigger]; +IF OBJECT_ID('[dbo].[test_trigger_alert]', 'U') IS NOT NULL DROP TABLE [dbo].[test_trigger_alert]; IF OBJECT_ID('[dbo].[negative_default_values]', 'U') IS NOT NULL DROP TABLE [dbo].[negative_default_values]; IF OBJECT_ID('[dbo].[animal]', 'U') IS NOT NULL DROP TABLE [dbo].[animal]; IF OBJECT_ID('[dbo].[default_pk]', 'U') IS NOT NULL DROP TABLE [dbo].[default_pk]; @@ -25,6 +27,7 @@ IF OBJECT_ID('[T_upsert]', 'U') IS NOT NULL DROP TABLE [T_upsert]; IF OBJECT_ID('[T_upsert_1]', 'U') IS NOT NULL DROP TABLE [T_upsert_1]; IF OBJECT_ID('[table.with.special.characters]', 'U') IS NOT NULL DROP TABLE [table.with.special.characters]; IF OBJECT_ID('[stranger ''table]', 'U') IS NOT NULL DROP TABLE [stranger 'table]; +IF OBJECT_ID('[foo1]', 'U') IS NOT NULL DROP TABLE [foo1]; CREATE TABLE [dbo].[profile] ( [id] [int] IDENTITY NOT NULL, @@ -375,3 +378,21 @@ CREATE TABLE [dbo].[stranger 'table] ( [id] [int], [stranger 'field] [varchar] (32) ); + +CREATE TABLE [dbo].[test_trigger] ( + [id] [int] IDENTITY NOT NULL, + [stringcol] [varchar](32) DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE [dbo].[test_trigger_alert] ( + [id] [int] IDENTITY NOT NULL, + [stringcol] [varchar](32) DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE [dbo].[foo1] ( + [id] [int] IDENTITY NOT NULL, + [bar] [varchar](32), + PRIMARY KEY (id) +); diff --git a/tests/data/oci.sql b/tests/data/oci.sql index 8355b22..17cc176 100644 --- a/tests/data/oci.sql +++ b/tests/data/oci.sql @@ -18,6 +18,7 @@ BEGIN EXECUTE IMMEDIATE 'DROP TABLE "constraints"'; EXCEPTION WHEN OTHERS THEN I BEGIN EXECUTE IMMEDIATE 'DROP TABLE "bool_values"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP TABLE "animal"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP TABLE "default_pk"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- +BEGIN EXECUTE IMMEDIATE 'DROP TABLE "default_multiple_pk"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP TABLE "document"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP TABLE "dossier"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP TABLE "employee"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;-- @@ -194,6 +195,13 @@ CREATE TABLE "default_pk" ( CONSTRAINT "default_pk_PK" PRIMARY KEY ("id") ENABLE ); +CREATE TABLE "default_multiple_pk" ( + "id" integer not null, + "second_key_column" char(10) not null, + "type" varchar2(255) not null, + CONSTRAINT "default_multiple_pk_PK" PRIMARY KEY ("id", "second_key_column") ENABLE +); + CREATE TABLE "document" ( "id" integer, "title" varchar2(255) not null, diff --git a/tests/data/postgres.sql b/tests/data/postgres.sql index 1950339..3be9e70 100644 --- a/tests/data/postgres.sql +++ b/tests/data/postgres.sql @@ -7,6 +7,7 @@ DROP TABLE IF EXISTS "composite_fk" CASCADE; DROP TABLE IF EXISTS "order_item" CASCADE; DROP TABLE IF EXISTS "item" CASCADE; +DROP SEQUENCE IF EXISTS "item_id_seq_2" CASCADE; DROP TABLE IF EXISTS "order_item_with_null_fk" CASCADE; DROP TABLE IF EXISTS "order" CASCADE; DROP TABLE IF EXISTS "order_with_null_fk" CASCADE; @@ -79,6 +80,7 @@ CREATE TABLE "item" ( name varchar(128) NOT NULL, category_id integer NOT NULL references "category"(id) on UPDATE CASCADE on DELETE CASCADE ); +CREATE SEQUENCE "item_id_seq_2"; CREATE TABLE "order" ( id serial not null primary key, diff --git a/tests/data/postgres10.sql b/tests/data/postgres10.sql new file mode 100644 index 0000000..fd18a66 --- /dev/null +++ b/tests/data/postgres10.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS "partitioned" CASCADE; + +CREATE TABLE "partitioned" ( + city_id int not null, + logdate date not null +) PARTITION BY RANGE ("logdate"); \ No newline at end of file diff --git a/tests/data/postgres12.sql b/tests/data/postgres12.sql new file mode 100644 index 0000000..cbf5eb4 --- /dev/null +++ b/tests/data/postgres12.sql @@ -0,0 +1,19 @@ +DROP TABLE IF EXISTS "generated" CASCADE; +DROP TABLE IF EXISTS "item_12" CASCADE; + +CREATE TABLE "generated" ( + id_always int GENERATED ALWAYS AS IDENTITY, + id_primary int GENERATED ALWAYS AS IDENTITY primary key, + id_default int GENERATED BY DEFAULT AS IDENTITY +); + +CREATE TABLE "item_12" ( + id int GENERATED ALWAYS AS IDENTITY primary key, + name varchar(128) NOT NULL, + category_id integer NOT NULL references "category"(id) on UPDATE CASCADE on DELETE CASCADE +); + +INSERT INTO "item_12" (name, category_id) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); +INSERT INTO "item_12" (name, category_id) VALUES ('Yii 1.1 Application Development Cookbook', 1); +INSERT INTO "item_12" (name, category_id) VALUES ('Ice Age', 2); +INSERT INTO "item_12" (name, category_id) VALUES ('Toy Story', 2); diff --git a/tests/data/travis/README.md b/tests/data/travis/README.md deleted file mode 100644 index a01c37f..0000000 --- a/tests/data/travis/README.md +++ /dev/null @@ -1,12 +0,0 @@ -This directory contains scripts for automated test runs via the [Travis CI](http://travis-ci.org) build service. They are used for the preparation of worker instances by setting up needed extensions and configuring database access. - -These scripts might be used to configure your own system for test runs. But since their primary purpose remains to support Travis in running the test cases, you would be best advised to stick to the setup notes in the tests themselves. - -The scripts are: - - - [`apc-setup.sh`](apc-setup.sh) - Installs and configures the [apc pecl extension](http://pecl.php.net/package/apc) - - [`cubrid-setup.sh`](cubrid-setup.sh) - Prepares the [CUBRID](http://www.cubrid.org/) server instance by installing the server and PHP PDO driver - - [`memcache-setup.sh`](memcache-setup.sh) - Compiles and installs the [memcache pecl extension](http://pecl.php.net/package/memcache) diff --git a/tests/data/travis/apc-setup.sh b/tests/data/travis/apc-setup.sh deleted file mode 100755 index d9b93a0..0000000 --- a/tests/data/travis/apc-setup.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -e - -if [ "$(expr "$TRAVIS_PHP_VERSION" "<" "5.5")" -eq 1 ]; then - yes '' | pecl install apc - #echo "extension = apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - echo "apc.enable_cli = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini -else - echo "Not installing APC as it is not available in PHP 5.5 anymore." -fi diff --git a/tests/data/travis/cubrid-setup.sh b/tests/data/travis/cubrid-setup.sh deleted file mode 100755 index 81f4087..0000000 --- a/tests/data/travis/cubrid-setup.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/sh -e -# -# install CUBRID DBMS - -if (php --version | grep -i HipHop > /dev/null); then - echo "Skipping CUBRID on HHVM" - exit 0 -fi - -CWD=$(pwd) - -# cubrid dbms -mkdir -p cubrid/$CUBRID_VERSION -cd cubrid -if (test -f $CUBRID_VERSION-linux.x86_64.tar.gz); then - echo "CUBRID is already downloaded" -else - wget http://ftp.cubrid.org/CUBRID_Engine/$CUBRID_VERSION-linux.x86_64.tar.gz -O $CUBRID_VERSION-linux.x86_64.tar.gz - cd $CUBRID_VERSION - tar xzf ../../$CUBRID_VERSION-linux.x86_64.tar.gz - cd ../.. -fi - -echo "setting cubrid env" -CUBRID=$CWD/cubrid/$CUBRID_VERSION/CUBRID -CUBRID_DATABASES=$CUBRID/databases -CUBRID_LANG=en_US - -ld_lib_path=`printenv LD_LIBRARY_PATH` || echo "LD_LIBRARY_PATH is empty" -if [ "$ld_lib_path" = "" ] -then - LD_LIBRARY_PATH=$CUBRID/lib -else - LD_LIBRARY_PATH=$CUBRID/lib:$LD_LIBRARY_PATH -fi - -SHLIB_PATH=$LD_LIBRARY_PATH -LIBPATH=$LD_LIBRARY_PATH -PATH=$CUBRID/bin:$CUBRID/cubridmanager:$PATH - -export CUBRID -export CUBRID_DATABASES -export CUBRID_LANG -export LD_LIBRARY_PATH -export SHLIB_PATH -export LIBPATH -export PATH - -# start cubrid -echo "starting cubrid..." -cubrid service start || echo "starting CUBRID services failed with exit code $?" -# create and start the demo db -$CUBRID/demo/make_cubrid_demo.sh || echo "setting up CUBRID demodb failed with exit code $?" -cubrid server start demodb || (echo "starting CUBRID demodb failed with exit code $?" && cat demodb_loaddb.log) - -echo "" -echo "Installed CUBRID $CUBRID_VERSION" -echo "" - -# cubrid pdo -install_pdo_cubrid() { - if (test "! (-f PDO_CUBRID-$CUBRID_PDO_VERSION.tgz)"); then - wget "http://pecl.php.net/get/PDO_CUBRID-$CUBRID_PDO_VERSION.tgz" -O PDO_CUBRID-$CUBRID_PDO_VERSION.tgz - fi - tar -zxf "PDO_CUBRID-$CUBRID_PDO_VERSION.tgz" - sh -c "cd PDO_CUBRID-$CUBRID_PDO_VERSION && phpize && ./configure --prefix=$CWD/cubrid/PDO_CUBRID-$CUBRID_PDO_VERSION && make" - - echo "extension=$CWD/cubrid/PDO_CUBRID-$CUBRID_PDO_VERSION/modules/pdo_cubrid.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - - return $? -} - -install_pdo_cubrid > ~/pdo_cubrid.log || ( echo "=== PDO CUBRID BUILD FAILED ==="; cat ~/pdo_cubrid.log; exit 1 ) - -echo "" -echo "Installed CUBRID PDO $CUBRID_PDO_VERSION" -echo "" - -cd .. diff --git a/tests/data/travis/cubrid-solo.rb b/tests/data/travis/cubrid-solo.rb deleted file mode 100755 index f5f0004..0000000 --- a/tests/data/travis/cubrid-solo.rb +++ /dev/null @@ -1,5 +0,0 @@ -file_cache_path "/tmp/chef-solo" -data_bag_path "/tmp/chef-solo/data_bags" -encrypted_data_bag_secret "/tmp/chef-solo/data_bag_key" -cookbook_path [ "/tmp/chef-solo/cookbooks" ] -role_path "/tmp/chef-solo/roles" \ No newline at end of file diff --git a/tests/data/travis/imagick-setup.sh b/tests/data/travis/imagick-setup.sh deleted file mode 100755 index df5266c..0000000 --- a/tests/data/travis/imagick-setup.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -e - -if [ $(phpenv version-name) = '5.4' ] || [ $(phpenv version-name) = '5.5' ] || [ $(phpenv version-name) = '5.6' ]; then - yes '' | pecl install imagick -fi diff --git a/tests/data/travis/memcache-setup.sh b/tests/data/travis/memcache-setup.sh deleted file mode 100755 index c266e94..0000000 --- a/tests/data/travis/memcache-setup.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -e - -if (php --version | grep -i HipHop > /dev/null); then - echo "skipping memcache on HHVM" -else - mkdir -p ~/.phpenv/versions/$(phpenv version-name)/etc - - # memcache is not available on PHP 7, memcacheD is. - if [ $(phpenv version-name) = '5.4' ] || [ $(phpenv version-name) = '5.5' ] ]; then - echo "extension=memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - else - echo "skipping memcache on php 7" - fi - - if [ $(phpenv version-name) = '5.6' ]; then - echo "skipping memcache on php 5.6 since it is broken for xenial" - else - echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - fi -fi diff --git a/tests/data/travis/mssql-setup.sh b/tests/data/travis/mssql-setup.sh deleted file mode 100644 index 70e3cff..0000000 --- a/tests/data/travis/mssql-setup.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -e - -if [[ $(phpenv version-name) = '7.3' ]] || [[ $(phpenv version-name) = '7.2' ]] || [[ $(phpenv version-name) = '7.1' ]]; then - sudo docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=YourStrong!Passw0rd' -p 1433:1433 -d microsoft/mssql-server-linux:2017-latest - - curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - - curl https://packages.microsoft.com/config/ubuntu/`lsb_release -r -s`/prod.list | sudo tee -a /etc/apt/sources.list - - sudo apt-get update -qq - sudo ACCEPT_EULA=Y apt-get -y install msodbcsql17 - # optional: for bcp and sqlcmd - sudo ACCEPT_EULA=Y apt-get -y install mssql-tools - echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bash_profile - echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc - source ~/.bashrc - # optional: for unixODBC development headers - sudo apt-get -y install unixodbc-dev - - sqlcmd -U sa -P YourStrong!Passw0rd -S localhost -Q "CREATE DATABASE yiitest" - - pecl install sqlsrv - pecl install pdo_sqlsrv -fi diff --git a/tests/data/travis/mysql-setup.sh b/tests/data/travis/mysql-setup.sh deleted file mode 100644 index b563aea..0000000 --- a/tests/data/travis/mysql-setup.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -e - -sudo service mysql stop - -docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7 - -while ! mysqladmin ping -h 127.0.0.1 --silent; do - sleep 1 -done diff --git a/tests/data/validators/models/FakedValidationModel.php b/tests/data/validators/models/FakedValidationModel.php index c21425b..bb74d96 100644 --- a/tests/data/validators/models/FakedValidationModel.php +++ b/tests/data/validators/models/FakedValidationModel.php @@ -44,14 +44,14 @@ class FakedValidationModel extends Model ]; } - public function inlineVal($attribute, $params = [], $validator) + public function inlineVal($attribute, $params, $validator, $current) { $this->inlineValArgs = \func_get_args(); return true; } - public function clientInlineVal($attribute, $params = [], $validator) + public function clientInlineVal($attribute, $params, $validator, $current) { return \func_get_args(); } @@ -88,4 +88,9 @@ class FakedValidationModel extends Model { return $this->inlineValArgs; } + + public function attributes() + { + return array_keys($this->attr); + } } diff --git a/tests/data/validators/models/ValidatorTestEachAndInlineMethodModel.php b/tests/data/validators/models/ValidatorTestEachAndInlineMethodModel.php new file mode 100644 index 0000000..e87af1b --- /dev/null +++ b/tests/data/validators/models/ValidatorTestEachAndInlineMethodModel.php @@ -0,0 +1,26 @@ + [function ($attribute, $params, $validator) { + if (is_array($this->$attribute)) { + $this->addError($attribute, 'Each & Inline validators bug'); + } + }]], + ]; + } +} diff --git a/tests/data/validators/models/ValidatorTestTypedPropModel.php b/tests/data/validators/models/ValidatorTestTypedPropModel.php new file mode 100644 index 0000000..0a27147 --- /dev/null +++ b/tests/data/validators/models/ValidatorTestTypedPropModel.php @@ -0,0 +1,15 @@ +assertInternalType('string', Yii::powered()); } + public function testCreateObjectArray() + { + Yii::$container = new Container(); + + $qux = Yii::createObject([ + '__class' => Qux::className(), + 'a' => 42, + ]); + + $this->assertInstanceOf(Qux::className(), $qux); + $this->assertSame(42, $qux->a); + } + public function testCreateObjectCallable() { Yii::$container = new Container(); @@ -99,7 +115,7 @@ class BaseYiiTest extends TestCase public function testCreateObjectEmptyArrayException() { $this->expectException('yii\base\InvalidConfigException'); - $this->expectExceptionMessage('Object configuration must be an array containing a "class" element.'); + $this->expectExceptionMessage('Object configuration must be an array containing a "class" or "__class" element.'); Yii::createObject([]); } @@ -112,6 +128,17 @@ class BaseYiiTest extends TestCase Yii::createObject(null); } + public function testDi3CompatibilityCreateDependentObject() + { + $object = Yii::createObject([ + '__class' => FooBaz::className(), + 'fooDependent' => ['__class' => FooDependentSubclass::className()], + ]); + + $this->assertInstanceOf(FooBaz::className(), $object); + $this->assertInstanceOf(FooDependentSubclass::className(), $object->fooDependent); + } + /** * @covers \yii\BaseYii::setLogger() * @covers \yii\BaseYii::getLogger() diff --git a/tests/framework/ChangeLogTest.php b/tests/framework/ChangeLogTest.php index fe03215..4a4d738 100644 --- a/tests/framework/ChangeLogTest.php +++ b/tests/framework/ChangeLogTest.php @@ -18,7 +18,7 @@ class ChangeLogTest extends TestCase public function changeProvider() { - $lines = explode("\n", file_get_contents(__DIR__ . '/../../framework/CHANGELOG.md')); + $lines = preg_split("~\R~", file_get_contents(__DIR__ . '/../../framework/CHANGELOG.md'), -1, PREG_SPLIT_NO_EMPTY); // Don't check last 1500 lines, they are old and often don't obey the standard. $lastIndex = count($lines) - 1500; diff --git a/tests/framework/ar/ActiveRecordTestTrait.php b/tests/framework/ar/ActiveRecordTestTrait.php index 691c939..5c189e0 100644 --- a/tests/framework/ar/ActiveRecordTestTrait.php +++ b/tests/framework/ar/ActiveRecordTestTrait.php @@ -9,6 +9,7 @@ namespace yiiunit\framework\ar; use yii\base\Event; use yii\db\BaseActiveRecord; +use yii\db\Expression; use yiiunit\data\ar\Customer; use yiiunit\data\ar\Order; use yiiunit\TestCase; @@ -104,6 +105,19 @@ trait ActiveRecordTestTrait $this->assertInstanceOf($customerClass, $customer); $this->assertEquals(2, $customer->id); + // find by expression + $customer = $customerClass::findOne(new Expression('[[id]] = :id', [':id' => 2])); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals('user2', $customer->name); + $customer = $customerClass::findOne( + new Expression('[[id]] = :id AND [[name]] = :name', [':id' => 2, ':name' => 'user1']) + ); + $this->assertNull($customer); + $customer = $customerClass::findOne(new Expression('[[id]] = :id', [':id' => 5])); + $this->assertNull($customer); + $customer = $customerClass::findOne(new Expression('[[name]] = :name', [':name' => 'user5'])); + $this->assertNull($customer); + // scope $this->assertCount(2, $customerClass::find()->active()->all()); $this->assertEquals(2, $customerClass::find()->active()->count()); diff --git a/tests/framework/base/ApplicationTest.php b/tests/framework/base/ApplicationTest.php index 4ef59bc..e6be5bb 100644 --- a/tests/framework/base/ApplicationTest.php +++ b/tests/framework/base/ApplicationTest.php @@ -66,6 +66,17 @@ class ApplicationTest extends TestCase $this->assertSame('Bootstrap with yii\base\Module', Yii::getLogger()->messages[3][0]); $this->assertSame('Bootstrap with Closure', Yii::getLogger()->messages[4][0]); } + + public function testModuleId() + { + $this->mockApplication(['id' => 'app-basic']); + $child = new Module('child'); + Yii::$app->setModules(['child' => $child]); + + $this->assertEquals('app-basic', Yii::$app->getModule('child')->module->id); + $this->assertEquals('', Yii::$app->getModule('child')->module->getUniqueId()); + $this->assertEquals('child', Yii::$app->getModule('child')->getUniqueId()); + } } class DispatcherMock extends Dispatcher diff --git a/tests/framework/base/ErrorExceptionTest.php b/tests/framework/base/ErrorExceptionTest.php new file mode 100644 index 0000000..fdf5888 --- /dev/null +++ b/tests/framework/base/ErrorExceptionTest.php @@ -0,0 +1,44 @@ +isXdebugStackAvailable()) { + $this->markTestSkipped('Xdebug is required.'); + } + try { + throw new ErrorException(); + } catch (ErrorException $e){ + $this->assertEquals(__FUNCTION__, $e->getTrace()[0]['function']); + } + } +} diff --git a/tests/framework/base/EventTest.php b/tests/framework/base/EventTest.php index 8a16cbe..670600f 100644 --- a/tests/framework/base/EventTest.php +++ b/tests/framework/base/EventTest.php @@ -104,6 +104,32 @@ class EventTest extends TestCase } /** + * @see https://github.com/yiisoft/yii2/issues/17300 + */ + public function testRunHandlersWithWildcard() + { + $triggered = false; + + Event::on('\yiiunit\framework\base\*', 'super*', function ($event) use (&$triggered) { + $triggered = true; + }); + + // instance-level + $this->assertFalse($triggered); + $someClass = new SomeClass(); + $someClass->emitEvent(); + $this->assertTrue($triggered); + + // reset + $triggered = false; + + // class-level + $this->assertFalse($triggered); + Event::trigger(SomeClass::className(), 'super.test'); + $this->assertTrue($triggered); + } + + /** * @see https://github.com/yiisoft/yii2/issues/17377 */ public function testNoFalsePositivesWithHasHandlers() diff --git a/tests/framework/base/ExposedSecurity.php b/tests/framework/base/ExposedSecurity.php index b538709..d653575 100644 --- a/tests/framework/base/ExposedSecurity.php +++ b/tests/framework/base/ExposedSecurity.php @@ -29,4 +29,12 @@ class ExposedSecurity extends Security { return parent::pbkdf2($algo, $password, $salt, $iterations, $length); } + + /** + * {@inheritdoc} + */ + public function shouldUseLibreSSL() + { + return parent::shouldUseLibreSSL(); + } } diff --git a/tests/framework/base/ModelTest.php b/tests/framework/base/ModelTest.php index 2e3c082..428f3fb 100644 --- a/tests/framework/base/ModelTest.php +++ b/tests/framework/base/ModelTest.php @@ -175,6 +175,18 @@ class ModelTest extends TestCase $this->assertTrue($speaker->isAttributeSafe('firstName')); } + public function testIsAttributeSafeForIntegerAttribute() + { + $model = new RulesModel(); + $model->rules = [ + [ + [123456], 'safe', + ] + ]; + + $this->assertTrue($model->isAttributeSafe(123456)); + } + public function testSafeScenarios() { $model = new RulesModel(); @@ -310,8 +322,8 @@ class ModelTest extends TestCase 'lastName' => ['Another one!'], ], $speaker->getErrors()); - $this->assertEquals(['Another one!', 'Something is wrong!', 'Totally wrong!'], $speaker->getErrorSummary(true)); - $this->assertEquals(['Another one!', 'Something is wrong!'], $speaker->getErrorSummary(false)); + $this->assertEquals(['Something is wrong!', 'Totally wrong!', 'Another one!'], $speaker->getErrorSummary(true)); + $this->assertEquals(['Something is wrong!', 'Another one!'], $speaker->getErrorSummary(false)); $speaker->clearErrors('firstName'); $this->assertEquals([ diff --git a/tests/framework/base/ModuleTest.php b/tests/framework/base/ModuleTest.php index f93768b..c4b3ae4 100644 --- a/tests/framework/base/ModuleTest.php +++ b/tests/framework/base/ModuleTest.php @@ -24,6 +24,19 @@ class ModuleTest extends TestCase $this->mockApplication(); } + public function testTrueParentModule() + { + $parent = new Module('parent'); + $child = new Module('child'); + $child2 = new Module('child2'); + + $parent->setModule('child', $child); + $parent->setModules(['child2' => $child2]); + + $this->assertEquals('parent', $child->module->id); + $this->assertEquals('parent', $child2->module->id); + } + public function testControllerPath() { $module = new TestModule('test'); @@ -162,27 +175,27 @@ class ModuleTest extends TestCase list($controller, $action) = $module->createController('base'); $this->assertSame('', $action); - $this->assertSame('base/default', $controller->uniqueId); + $this->assertSame('app/base/default', $controller->uniqueId); list($controller, $action) = $module->createController('base/default'); $this->assertSame('', $action); - $this->assertSame('base/default', $controller->uniqueId); + $this->assertSame('app/base/default', $controller->uniqueId); list($controller, $action) = $module->createController('base/other'); $this->assertSame('', $action); - $this->assertSame('base/other', $controller->uniqueId); + $this->assertSame('app/base/other', $controller->uniqueId); list($controller, $action) = $module->createController('base/default/index'); $this->assertSame('index', $action); - $this->assertSame('base/default', $controller->uniqueId); + $this->assertSame('app/base/default', $controller->uniqueId); list($controller, $action) = $module->createController('base/other/index'); $this->assertSame('index', $action); - $this->assertSame('base/other', $controller->uniqueId); + $this->assertSame('app/base/other', $controller->uniqueId); list($controller, $action) = $module->createController('base/other/someaction'); $this->assertSame('someaction', $action); - $this->assertSame('base/other', $controller->uniqueId); + $this->assertSame('app/base/other', $controller->uniqueId); $controller = $module->createController('bases/default/index'); $this->assertFalse($controller); diff --git a/tests/framework/base/SecurityTest.php b/tests/framework/base/SecurityTest.php index 3cc51cf..e847140 100644 --- a/tests/framework/base/SecurityTest.php +++ b/tests/framework/base/SecurityTest.php @@ -96,6 +96,11 @@ class SecurityTest extends TestCase parent::tearDown(); } + private function isWindows() + { + return DIRECTORY_SEPARATOR !== '/'; + } + // Tests : public function testHashData() @@ -943,13 +948,17 @@ TEXT; } } // there is no /dev/urandom on windows so we expect this to fail - if (DIRECTORY_SEPARATOR === '\\' && $functions['random_bytes'] === false && $functions['openssl_random_pseudo_bytes'] === false && $functions['mcrypt_create_iv'] === false) { + if ($this->isWindows() && $functions['random_bytes'] === false && $functions['openssl_random_pseudo_bytes'] === false && $functions['mcrypt_create_iv'] === false) { $this->expectException('yii\base\Exception'); $this->expectExceptionMessage('Unable to generate a random key'); } // Function mcrypt_create_iv() is deprecated since PHP 7.1 if (version_compare(PHP_VERSION, '7.1.0alpha', '>=') && $functions['random_bytes'] === false && $functions['mcrypt_create_iv'] === true) { - $this->markTestSkipped('Function mcrypt_create_iv() is deprecated as of PHP 7.1'); + if ($functions['openssl_random_pseudo_bytes'] === false) { + $this->markTestSkipped('Function mcrypt_create_iv() is deprecated as of PHP 7.1'); + } elseif (!$this->security->shouldUseLibreSSL() && !$this->isWindows()) { + $this->markTestSkipped('Function openssl_random_pseudo_bytes need LibreSSL version >=2.1.5 or Windows system on server'); + } } static::$functions = $functions; @@ -1013,7 +1022,7 @@ TEXT; 'DIRECTORY_SEPARATOR', "ini_get('open_basedir')", ]; - if (DIRECTORY_SEPARATOR === '/') { + if ($this->isWindows()) { $tests[] = "sprintf('%o', lstat(PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom')['mode'] & 0170000)"; $tests[] = "bin2hex(file_get_contents(PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom', false, null, 0, 8))"; } @@ -1072,7 +1081,7 @@ TEXT; 20, '4b007901b765489abead49d926f721d065a429c1', ], - getenv('TRAVIS') == true ? [ + getenv('GITHUB_ACTIONS') == true ? [ 'sha1', 'password', 'salt', diff --git a/tests/framework/caching/FileCacheTest.php b/tests/framework/caching/FileCacheTest.php index 3f19ee6..55f2fca 100644 --- a/tests/framework/caching/FileCacheTest.php +++ b/tests/framework/caching/FileCacheTest.php @@ -53,6 +53,36 @@ class FileCacheTest extends CacheTestCase $this->assertFalse($cache->get('expire_testa')); } + public function testKeyPrefix() + { + $keyPrefix = 'foobar'; + $key = uniqid('uid-cache_'); + $cache = $this->getCacheInstance(); + $cache->flush(); + + $cache->directoryLevel = 1; + $cache->keyPrefix = $keyPrefix; + $normalizeKey = $cache->buildKey($key); + $expectedDirectoryName = substr($normalizeKey, 6, 2); + + $value = \time(); + + $refClass = new \ReflectionClass($cache); + + $refMethodGetCacheFile = $refClass->getMethod('getCacheFile'); + $refMethodGetCacheFile->setAccessible(true); + $refMethodGet = $refClass->getMethod('get'); + $refMethodSet = $refClass->getMethod('set'); + + $cacheFile = $refMethodGetCacheFile->invoke($cache, $normalizeKey); + + $this->assertTrue($refMethodSet->invoke($cache, $key, $value)); + $this->assertContains($keyPrefix, basename($cacheFile)); + $this->assertEquals($expectedDirectoryName, basename(dirname($cacheFile)), $cacheFile); + $this->assertTrue(is_dir(dirname($cacheFile)), 'File not found ' . $cacheFile); + $this->assertEquals($value, $refMethodGet->invoke($cache, $key)); + } + public function testCacheRenewalOnDifferentOwnership() { $TRAVIS_SECOND_USER = getenv('TRAVIS_SECOND_USER'); diff --git a/tests/framework/caching/MemCacheTest.php b/tests/framework/caching/MemCacheTest.php index 7796b80..0487a16 100644 --- a/tests/framework/caching/MemCacheTest.php +++ b/tests/framework/caching/MemCacheTest.php @@ -41,16 +41,16 @@ class MemCacheTest extends CacheTestCase public function testExpire() { - if (getenv('TRAVIS') == 'true') { - $this->markTestSkipped('Can not reliably test memcache expiry on travis-ci.'); + if (getenv('GITHUB_ACTIONS') == 'true') { + $this->markTestSkipped('Can not reliably test memcache expiry on GitHub actions.'); } parent::testExpire(); } public function testExpireAdd() { - if (getenv('TRAVIS') == 'true') { - $this->markTestSkipped('Can not reliably test memcache expiry on travis-ci.'); + if (getenv('GITHUB_ACTIONS') == 'true') { + $this->markTestSkipped('Can not reliably test memcache expiry on GitHub actions.'); } parent::testExpireAdd(); } diff --git a/tests/framework/caching/MemCachedTest.php b/tests/framework/caching/MemCachedTest.php index 1bbd0ea..78f6678 100644 --- a/tests/framework/caching/MemCachedTest.php +++ b/tests/framework/caching/MemCachedTest.php @@ -41,16 +41,16 @@ class MemCachedTest extends CacheTestCase public function testExpire() { - if (getenv('TRAVIS') == 'true') { - $this->markTestSkipped('Can not reliably test memcached expiry on travis-ci.'); + if (getenv('GITHUB_ACTIONS') == 'true') { + $this->markTestSkipped('Can not reliably test memcached expiry on GitHub actions.'); } parent::testExpire(); } public function testExpireAdd() { - if (getenv('TRAVIS') == 'true') { - $this->markTestSkipped('Can not reliably test memcached expiry on travis-ci.'); + if (getenv('GITHUB_ACTIONS') == 'true') { + $this->markTestSkipped('Can not reliably test memcached expiry on GitHub actions.'); } parent::testExpireAdd(); } diff --git a/tests/framework/console/ControllerTest.php b/tests/framework/console/ControllerTest.php index 6220a53..d5ce60c 100644 --- a/tests/framework/console/ControllerTest.php +++ b/tests/framework/console/ControllerTest.php @@ -7,8 +7,13 @@ namespace yiiunit\framework\console; +use RuntimeException; +use yii\console\Exception; +use yiiunit\framework\console\stubs\DummyService; use Yii; +use yii\base\InlineAction; use yii\base\Module; +use yii\console\Application; use yii\console\Request; use yii\helpers\Console; use yiiunit\TestCase; @@ -18,6 +23,9 @@ use yiiunit\TestCase; */ class ControllerTest extends TestCase { + /** @var FakeController */ + private $controller; + protected function setUp() { parent::setUp(); @@ -29,6 +37,15 @@ class ControllerTest extends TestCase ]; } + public function testBindArrayToActionParams() + { + $controller = new FakeController('fake', Yii::$app); + + $params = ['test' => []]; + $this->assertEquals([], $controller->runAction('aksi4', $params)); + $this->assertEquals([], $controller->runAction('aksi4', $params)); + } + public function testBindActionParams() { $controller = new FakeController('fake', Yii::$app); @@ -69,11 +86,132 @@ class ControllerTest extends TestCase $this->assertEquals('from params', $fromParam); $this->assertEquals('notdefault', $other); + $params = ['from params', 'notdefault']; + list($fromParam, $other) = $controller->run('trimargs', $params); + $this->assertEquals('from params', $fromParam); + $this->assertEquals('notdefault', $other); + $params = ['avaliable']; $message = Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', ['missing'])]); $this->expectException('yii\console\Exception'); $this->expectExceptionMessage($message); $result = $controller->runAction('aksi3', $params); + + } + + public function testNullableInjectedActionParams() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $this->mockApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionNullableInjection'); + $params = []; + $args = $this->controller->bindActionParams($injectionAction, $params); + $this->assertEquals(\Yii::$app->request, $args[0]); + $this->assertNull($args[1]); + } + + public function testInjectionContainerException() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $this->mockApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + \Yii::$container->set(DummyService::className(), function() { throw new \RuntimeException('uh oh'); }); + + $this->expectException(get_class(new RuntimeException())); + $this->expectExceptionMessage('uh oh'); + $this->controller->bindActionParams($injectionAction, $params); + } + + public function testUnknownInjection() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $this->mockApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + \Yii::$container->clear(DummyService::className()); + $this->expectException(get_class(new Exception())); + $this->expectExceptionMessage('Could not load required service: dummyService'); + $this->controller->bindActionParams($injectionAction, $params); + } + + public function testInjectedActionParams() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $this->mockApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + \Yii::$container->set(DummyService::className(), DummyService::className()); + $args = $this->controller->bindActionParams($injectionAction, $params); + $this->assertEquals($params['before'], $args[0]); + $this->assertEquals(\Yii::$app->request, $args[1]); + $this->assertEquals('Component: yii\console\Request $request', \Yii::$app->requestedParams['request']); + $this->assertEquals($params['between'], $args[2]); + $this->assertInstanceOf(DummyService::className(), $args[3]); + $this->assertEquals('Container DI: yiiunit\framework\console\stubs\DummyService $dummyService', \Yii::$app->requestedParams['dummyService']); + $this->assertNull($args[4]); + $this->assertEquals('Unavailable service: post', \Yii::$app->requestedParams['post']); + $this->assertEquals($params['after'], $args[5]); + } + + public function testInjectedActionParamsFromModule() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + $module = new \yii\base\Module('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $module->set('yii\data\DataProviderInterface', [ + 'class' => \yii\data\ArrayDataProvider::className(), + ]); + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', $module); + $this->mockWebApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionModuleServiceInjection'); + $args = $this->controller->bindActionParams($injectionAction, []); + $this->assertInstanceOf(\yii\data\ArrayDataProvider::className(), $args[0]); + $this->assertEquals('Module yii\base\Module DI: yii\data\DataProviderInterface $dataProvider', \Yii::$app->requestedParams['dataProvider']); } public function assertResponseStatus($status, $response) diff --git a/tests/framework/console/FakeController.php b/tests/framework/console/FakeController.php index 7ca0d55..d3a39b2 100644 --- a/tests/framework/console/FakeController.php +++ b/tests/framework/console/FakeController.php @@ -84,6 +84,11 @@ class FakeController extends Controller return $this->testArray; } + public function actionTrimargs($param1 = null) + { + return func_get_args(); + } + public function actionWithComplexTypeHint(self $typedArgument, $simpleArgument) { return $simpleArgument; diff --git a/tests/framework/console/FakePhp71Controller.php b/tests/framework/console/FakePhp71Controller.php new file mode 100644 index 0000000..985940d --- /dev/null +++ b/tests/framework/console/FakePhp71Controller.php @@ -0,0 +1,29 @@ +_cacheController = Yii::createObject([ - 'class' => 'yiiunit\framework\console\controllers\SilencedCacheController', - 'interactive' => false, - ], [null, null]); //id and module are null - $databases = self::getParam('databases'); $config = $databases[$this->driverName]; $pdoDriver = 'pdo_' . $this->driverName; @@ -73,6 +68,11 @@ class CacheControllerTest extends TestCase ], ]); + $this->_cacheController = Yii::createObject([ + 'class' => 'yiiunit\framework\console\controllers\SilencedCacheController', + 'interactive' => false, + ], [null, null]); //id and module are null + if (isset($config['fixture'])) { Yii::$app->db->open(); $lines = explode(';', file_get_contents($config['fixture'])); diff --git a/tests/framework/console/controllers/FixtureControllerTest.php b/tests/framework/console/controllers/FixtureControllerTest.php index ac805f4..ec2b00e 100644 --- a/tests/framework/console/controllers/FixtureControllerTest.php +++ b/tests/framework/console/controllers/FixtureControllerTest.php @@ -237,11 +237,9 @@ class FixtureControllerTest extends DatabaseTestCase $this->_fixtureController->actionLoad(['*']); - $this->assertEquals([ - SecondIndependentActiveFixture::className(), - FirstIndependentActiveFixture::className(), - DependentActiveFixture::className(), - ], FixtureStorage::$activeFixtureSequence); + $lastFixture = end(FixtureStorage::$activeFixtureSequence); + + $this->assertEquals(DependentActiveFixture::className(), $lastFixture); } } diff --git a/tests/framework/console/controllers/HelpControllerTest.php b/tests/framework/console/controllers/HelpControllerTest.php index 5c43a21..c86d6ef 100644 --- a/tests/framework/console/controllers/HelpControllerTest.php +++ b/tests/framework/console/controllers/HelpControllerTest.php @@ -128,6 +128,7 @@ action:route to action --interactive: whether to run the command interactively. --color: whether to enable ANSI color in the output.If not set, ANSI color will only be enabled for terminals that support it. --help: whether to display help information about current command. +--silent-exit-on-exception: if true - script finish with `ExitCode\:\:OK` in case of exception.false - `ExitCode\:\:UNSPECIFIED_ERROR`.Default\: `YII_ENV_TEST` STRING , $result); diff --git a/tests/framework/console/controllers/MigrateControllerTest.php b/tests/framework/console/controllers/MigrateControllerTest.php index 4f3e1ea..e93e2a2 100644 --- a/tests/framework/console/controllers/MigrateControllerTest.php +++ b/tests/framework/console/controllers/MigrateControllerTest.php @@ -9,6 +9,7 @@ namespace yiiunit\framework\console\controllers; use Yii; use yii\console\controllers\MigrateController; +use yii\console\ExitCode; use yii\db\Migration; use yii\db\Query; use yii\helpers\Inflector; @@ -96,6 +97,7 @@ class MigrateControllerTest extends TestCase list($config, $namespace, $class) = $this->prepareMigrationNameData($migrationName); $this->runMigrateControllerAction('create', [$migrationName], $config); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertFileContentJunction($expectedFile, $class, $junctionTable, $firstTable, $secondTable, $namespace); } @@ -131,6 +133,14 @@ class MigrateControllerTest extends TestCase price:money(11,2):notNull, parenthesis_in_comment:string(255):notNull:comment(\'Name of set (RU)\')', ], + 'create_fields_with_col_method_after_default_value' => [ + 'fields' => 'id:primaryKey, + title:string(10):notNull:unique:defaultValue("test"):after("id"), + body:text:notNull:defaultValue("test"):after("title"), + address:text:notNull:defaultValue("test"):after("body"), + address2:text:notNull:defaultValue(\'te:st\'):after("address"), + address3:text:notNull:defaultValue(\':te:st:\'):after("address2")', + ], 'create_title_pk' => [ 'fields' => 'title:primaryKey,body:text:notNull,price:money(11,2)', ], @@ -204,7 +214,7 @@ class MigrateControllerTest extends TestCase return [ ['default', 'DefaultTest', 'default', []], - // underscore + table name = case keeped + // underscore + table name = case kept ['create_test', 'create_test_table', 'test', []], ['create_test', 'create_test__table', 'test_', []], ['create_test', 'create_TEST_table', 'TEST', []], @@ -236,6 +246,9 @@ class MigrateControllerTest extends TestCase ['create_title_with_comma_default_values', 'create_test_table', 'test', $params['create_title_with_comma_default_values']], ['create_field_with_colon_default_values', 'create_test_table', 'test', $params['create_field_with_colon_default_values']], + // @see https://github.com/yiisoft/yii2/issues/18303 + ['create_fields_with_col_method_after_default_value', 'create_test_table', 'test', $params['create_fields_with_col_method_after_default_value']], + ['drop_test', 'drop_test_table', 'test', []], ['drop_test', 'drop_test__table', 'test_', []], ['drop_test', 'drop_TEST_table', 'TEST', []], @@ -359,6 +372,7 @@ class MigrateControllerTest extends TestCase $this->createMigration(str_repeat('a', 180)); $result = $this->runMigrateControllerAction('up'); + $this->assertSame(ExitCode::UNSPECIFIED_ERROR, $this->getExitCode()); $this->assertContains('The migration name', $result); $this->assertContains('is too long. Its not possible to apply this migration.', $result); @@ -374,6 +388,7 @@ class MigrateControllerTest extends TestCase $this->createMigration(str_repeat('a', 180)); $result = $this->runMigrateControllerAction('up'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertContains('1 migration was applied.', $result); $this->assertContains('Migrated up successfully.', $result); @@ -407,18 +422,15 @@ class MigrateControllerTest extends TestCase $this->switchDbConnection($db); } - Yii::$app->db->createCommand('create table hall_of_fame(id int, string varchar(255))') - ->execute(); + Yii::$app->db->createCommand('create table hall_of_fame(id int, string varchar(255))')->execute(); - Yii::$app->db->createCommand("insert into hall_of_fame values(1, 'Qiang Xue');") - ->execute(); - Yii::$app->db->createCommand("insert into hall_of_fame values(2, 'Alexander Makarov');") - ->execute(); + Yii::$app->db->createCommand("insert into hall_of_fame values(1, 'Qiang Xue');")->execute(); + Yii::$app->db->createCommand("insert into hall_of_fame values(2, 'Alexander Makarov');")->execute(); - Yii::$app->db->createCommand('create view view_hall_of_fame as select * from hall_of_fame') - ->execute(); + Yii::$app->db->createCommand('create view view_hall_of_fame as select * from hall_of_fame')->execute(); $result = $this->runMigrateControllerAction('fresh'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); // Drop worked $this->assertContains('Table hall_of_fame dropped.', $result); diff --git a/tests/framework/console/controllers/MigrateControllerTestTrait.php b/tests/framework/console/controllers/MigrateControllerTestTrait.php index c9e32bd..7b8cd16 100644 --- a/tests/framework/console/controllers/MigrateControllerTestTrait.php +++ b/tests/framework/console/controllers/MigrateControllerTestTrait.php @@ -9,6 +9,7 @@ namespace yiiunit\framework\console\controllers; use Yii; use yii\console\controllers\BaseMigrateController; +use yii\console\ExitCode; use yii\helpers\FileHelper; use yii\helpers\StringHelper; use yiiunit\TestCase; @@ -37,7 +38,16 @@ trait MigrateControllerTestTrait * @var string test migration namespace */ protected $migrationNamespace; + /** + * @var int|null migration controller exit code + */ + protected $migrationExitCode; + + public function getExitCode() + { + return $this->migrationExitCode; + } public function setUpMigrationPath() { @@ -91,7 +101,7 @@ trait MigrateControllerTestTrait $controller = $this->createMigrateController($config); ob_start(); ob_implicit_flush(false); - $controller->run($actionID, $args); + $this->migrationExitCode = $controller->run($actionID, $args); return ob_get_clean(); } @@ -215,6 +225,7 @@ CODE; { $migrationName = 'test_migration'; $this->runMigrateControllerAction('create', [$migrationName]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $files = FileHelper::findFiles($this->migrationPath); $this->assertCount(1, $files, 'Unable to create new migration!'); $this->assertContains($migrationName, basename($files[0]), 'Wrong migration name!'); @@ -226,6 +237,7 @@ CODE; $this->createMigration('test_up2'); $this->runMigrateControllerAction('up'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', 'm*_test_up1', 'm*_test_up2']); } @@ -239,6 +251,7 @@ CODE; $this->createMigration('test_down2'); $this->runMigrateControllerAction('up', [1]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', 'm*_test_down1']); } @@ -252,7 +265,9 @@ CODE; $this->createMigration('test_down_count2'); $this->runMigrateControllerAction('up'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->runMigrateControllerAction('down', [1]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', 'm*_test_down_count1']); } @@ -266,7 +281,9 @@ CODE; $this->createMigration('test_down_all2'); $this->runMigrateControllerAction('up'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->runMigrateControllerAction('down', ['all']); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base']); } @@ -282,8 +299,10 @@ CODE; $this->createMigration('test_history1'); $this->createMigration('test_history2'); $this->runMigrateControllerAction('up'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $output = $this->runMigrateControllerAction('history'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertContains('_test_history1', $output); $this->assertContains('_test_history2', $output); } @@ -296,11 +315,14 @@ CODE; $this->createMigration('test_new1'); $output = $this->runMigrateControllerAction('new'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertContains('_test_new1', $output); $this->runMigrateControllerAction('up'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $output = $this->runMigrateControllerAction('new'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertNotContains('_test_new1', $output); } @@ -310,6 +332,7 @@ CODE; $this->createMigration('test_mark1', $version); $this->runMigrateControllerAction('mark', [$version]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', 'm*_test_mark1']); } @@ -320,9 +343,11 @@ CODE; $this->createMigration('test_mark1', $version); $this->runMigrateControllerAction('up'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', 'm*_test_mark1']); $this->runMigrateControllerAction('mark', [BaseMigrateController::BASE_MIGRATION]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base']); } @@ -332,6 +357,7 @@ CODE; $this->createMigration('to1', $version); $this->runMigrateControllerAction('to', [$version]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', 'm*_to1']); } @@ -343,8 +369,10 @@ CODE; { $this->createMigration('test_redo1'); $this->runMigrateControllerAction('up'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->runMigrateControllerAction('redo'); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', 'm*_test_redo1']); } @@ -362,6 +390,7 @@ CODE; 'migrationPath' => null, 'migrationNamespaces' => [$this->migrationNamespace], ]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $files = FileHelper::findFiles($this->migrationPath); $fileContent = file_get_contents($files[0]); $this->assertContains("namespace {$this->migrationNamespace};", $fileContent); @@ -374,6 +403,7 @@ CODE; 'migrationPath' => $this->migrationPath, 'migrationNamespaces' => [$this->migrationNamespace], ]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $files = FileHelper::findFiles($this->migrationPath); $fileContent = file_get_contents($files[0]); $this->assertContains("namespace {$this->migrationNamespace};", $fileContent); @@ -385,6 +415,7 @@ CODE; 'migrationPath' => $this->migrationPath, 'migrationNamespaces' => [$this->migrationNamespace], ]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $files = FileHelper::findFiles($this->migrationPath); $fileContent = file_get_contents($files[0]); $this->assertNotContains("namespace {$this->migrationNamespace};", $fileContent); @@ -402,6 +433,7 @@ CODE; 'migrationPath' => null, 'migrationNamespaces' => [$this->migrationNamespace], ]); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_*_base', @@ -424,7 +456,9 @@ CODE; 'migrationNamespaces' => [$this->migrationNamespace], ]; $this->runMigrateControllerAction('up', [], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->runMigrateControllerAction('down', [1], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_*_base', @@ -449,8 +483,10 @@ CODE; $this->createNamespaceMigration('history1'); $this->createNamespaceMigration('history2'); $this->runMigrateControllerAction('up', [], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $output = $this->runMigrateControllerAction('history', [], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertRegExp('/' . preg_quote($this->migrationNamespace) . '.*History1/s', $output); $this->assertRegExp('/' . preg_quote($this->migrationNamespace) . '.*History2/s', $output); } @@ -469,6 +505,7 @@ CODE; $this->createNamespaceMigration('mark1', $version); $this->runMigrateControllerAction('mark', [$this->migrationNamespace . '\\M' . $version], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', $this->migrationNamespace . '\\M*Mark1']); } @@ -487,6 +524,7 @@ CODE; $this->createNamespaceMigration('to1', $version); $this->runMigrateControllerAction('to', [$this->migrationNamespace . '\\M' . $version], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', $this->migrationNamespace . '\\M*To1']); } @@ -510,10 +548,12 @@ CODE; // yii migrate/up 1 $this->runMigrateControllerAction('up', [1], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory(['m*_base', 'm010101_000001_app_migration1']); // yii migrate/up $this->runMigrateControllerAction('up', [], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', @@ -524,6 +564,7 @@ CODE; // yii migrate/to m010101_000002_ext_migration1 $this->runMigrateControllerAction('to', ['m010101_000002_ext_migration1'], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', @@ -531,6 +572,7 @@ CODE; // yii migrate/mark M010101000004NsMigration $this->runMigrateControllerAction('mark', ['m010101_000003_app_migration2'], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', @@ -540,6 +582,7 @@ CODE; // yii migrate/up $this->runMigrateControllerAction('up', [], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', @@ -550,6 +593,7 @@ CODE; // yii migrate/redo 2 $this->runMigrateControllerAction('redo', [2], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', @@ -560,6 +604,7 @@ CODE; // yii migrate/down $this->runMigrateControllerAction('down', [], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', @@ -569,6 +614,7 @@ CODE; // yii migrate/redo $this->runMigrateControllerAction('redo', [], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', @@ -578,6 +624,7 @@ CODE; // yii migrate/down 2 $this->runMigrateControllerAction('down', [2], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', @@ -585,6 +632,7 @@ CODE; // yii migrate/create app_migration3 $this->runMigrateControllerAction('create', ['app_migration3'], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', @@ -592,6 +640,7 @@ CODE; // yii migrate/up $this->runMigrateControllerAction('up', [], $controllerConfig); + $this->assertSame(ExitCode::OK, $this->getExitCode()); $this->assertMigrationHistory([ 'm*_base', 'm010101_000001_app_migration1', diff --git a/tests/framework/console/stubs/DummyService.php b/tests/framework/console/stubs/DummyService.php new file mode 100644 index 0000000..b1be9fd --- /dev/null +++ b/tests/framework/console/stubs/DummyService.php @@ -0,0 +1,16 @@ +setRows([])->setScreenWidth(200)->run() ); } + + public function testEmptyAndZeroTableCell() + { + $table = new Table(); + + $expected = <<<'EXPECTED' +╔═══════╤═══════╗ +║ test1 │ test2 ║ +╟───────┼───────╢ +║ 0 │ ║ +╟───────┼───────╢ +║ 0.0 │ ║ +╚═══════╧═══════╝ + +EXPECTED; + + $this->assertEqualsWithoutLE( + $expected, + $table + ->setHeaders(['test1', 'test2']) + ->setRows([ + ['0', []], + ['0.0', []], + ]) + ->setScreenWidth(200) + ->run() + ); + } + + public function testColorizedInput() + { + $table = new Table(); + + $expected = <<<"EXPECTED" +╔═══════╤═══════╤══════════╗ +║ test1 │ test2 │ test3 ║ +╟───────┼───────┼──────────╢ +║ col1 │ \e[33mcol2\e[0m │ col3 ║ +╟───────┼───────┼──────────╢ +║ col1 │ col2 │ • col3-0 ║ +║ │ │ • \e[31mcol3-1\e[0m ║ +║ │ │ • col3-2 ║ +╚═══════╧═══════╧══════════╝ + +EXPECTED; + + $this->assertEqualsWithoutLE( + $expected, + $table + ->setHeaders(['test1', 'test2', 'test3']) + ->setRows([ + ['col1', Console::renderColoredString('%ycol2%n'), 'col3'], + ['col1', 'col2', ['col3-0', Console::renderColoredString('%rcol3-1%n'), 'col3-2']], + ]) + ->run() + ); + } + + public function testColorizedInputStripsANSIMarkersInternally() + { + $table = new Table(); + + $table + ->setHeaders(['t1', 't2', 't3']) + ->setRows([ + ['col1', Console::renderColoredString('%ycol2%n'), 'col3'], + ['col1', 'col2', ['col3-0', Console::renderColoredString('%rcol3-1%n'), 'col3-2']], + ]) + ->setScreenWidth(200) + ->run(); + + $columnWidths = \PHPUnit_Framework_Assert::readAttribute($table, "columnWidths"); + + $this->assertArrayHasKey(1, $columnWidths); + $this->assertEquals(4+2, $columnWidths[1]); + $this->assertArrayHasKey(2, $columnWidths); + $this->assertEquals(8+2, $columnWidths[2]); + } + + public function testCalculateRowHeightShouldNotThrowDivisionByZeroException() + { + $rows = [ + ['XXXXXX', 'XXXXXXXXXXXXXXXXXXXX', '', '', 'XXXXXXXXXXXXXXXXXX', 'X', 'XXX'], + ['XXXXXX', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', '', '', ''], + ['XXXXXX', 'XXXXXXXXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', '', '', '', ''], + ]; + + $table = Table::widget([ + 'headers' => ['XX', 'XXXX'], + 'rows' => $rows + ]); + $this->assertEqualsWithoutLE($table, $table); + } + + public function testLineBreakTableCell() + { + $table = new Table(); + + $expected = <<<"EXPECTED" +╔══════════════════════╗ +║ test ║ +╟──────────────────────╢ +║ AAAAAAAAAAAAAAAAAAAA ║ +║ BBBBBBBBBBBBBBBBBBBB ║ +║ CCCCC ║ +╟──────────────────────╢ +║ • AAAAAAAAAAAAAAAAAA ║ +║ BBBBBBB ║ +║ • CCCCCCCCCCCCCCCCCC ║ +║ DDDDDDD ║ +╚══════════════════════╝ + +EXPECTED; + + $this->assertEqualsWithoutLE( + $expected, + $table->setHeaders(['test']) + ->setRows([ + ['AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBCCCCC'], + [[ + 'AAAAAAAAAAAAAAAAAABBBBBBB', + 'CCCCCCCCCCCCCCCCCCDDDDDDD', + ]], + ]) + ->setScreenWidth(25) + ->run() + ); + } + + public function testColorizedLineBreakTableCell() + { + $table = new Table(); + + $expected = <<<"EXPECTED" +╔══════════════════════╗ +║ test ║ +╟──────────────────────╢ +║ \e[33mAAAAAAAAAAAAAAAAAAAA\e[0m ║ +║ \e[33mBBBBBBBBBBBBBBBBBBBB\e[0m ║ +║ \e[33mCCCCC\e[0m ║ +╟──────────────────────╢ +║ \e[31mAAAAAAAAAAAAAAAAAAAA\e[0m ║ +║ \e[32mBBBBBBBBBBBBBBBBBBBB\e[0m ║ +║ \e[34mCCCCC\e[0m ║ +╟──────────────────────╢ +║ • \e[31mAAAAAAAAAAAAAAAAAA\e[0m ║ +║ \e[31mBBBBBBB\e[0m ║ +║ • \e[33mCCCCCCCCCCCCCCCCCC\e[0m ║ +║ \e[33mDDDDDDD\e[0m ║ +╟──────────────────────╢ +║ • \e[35mAAAAAAAAAAAAAAAAAA\e[0m ║ +║ \e[31mBBBBBBB\e[0m ║ +║ • \e[32mCCCCCCCCCCCCCCCCCC\e[0m ║ +║ \e[34mDDDDDDD\e[0m ║ +╚══════════════════════╝ + +EXPECTED; + + $this->assertEqualsWithoutLE( + $expected, + $table->setHeaders(['test']) + ->setRows([ + [Console::renderColoredString('%yAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBCCCCC%n')], + [Console::renderColoredString('%rAAAAAAAAAAAAAAAAAAAA%gBBBBBBBBBBBBBBBBBBBB%bCCCCC%n')], + [[ + Console::renderColoredString('%rAAAAAAAAAAAAAAAAAABBBBBBB%n'), + Console::renderColoredString('%yCCCCCCCCCCCCCCCCCCDDDDDDD%n'), + ]], + [[ + Console::renderColoredString('%mAAAAAAAAAAAAAAAAAA%rBBBBBBB%n'), + Console::renderColoredString('%gCCCCCCCCCCCCCCCCCC%bDDDDDDD%n'), + ]], + ]) + ->setScreenWidth(25) + ->run() + ); + } + + /** + * @param $smallString + * @dataProvider dataMinimumWidth + */ + public function testMinimumWidth($smallString) + { + $bigString = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; + + (new Table()) + ->setHeaders(['t1', 't2', '']) + ->setRows([ + [$bigString, $bigString, $smallString], + ]) + ->setScreenWidth(20) + ->run(); + + // Without exceptions + $this->assertTrue(true); + } + + public function dataMinimumWidth() + { + return [ + ['X'], + [''], + [['X', 'X', 'X']], + [[]], + [['']] + ]; + } + + public function testTableWithAnsiFormat() + { + $table = new Table(); + + // test fullwidth chars + // @see https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms + $expected = <<assertEqualsWithoutLE($expected, $table->setHeaders(['test1', 'test2', Console::ansiFormat('test3', [Console::FG_RED])]) + ->setRows([ + [Console::ansiFormat('testcontent11', [Console::FG_BLUE]), Console::ansiFormat('testcontent12', [Console::FG_YELLOW]), 'testcontent13'], + ['testcontent21', 'testcontent22', [ + 'a', + Console::ansiFormat('b', [Console::FG_PURPLE]), + Console::ansiFormat('c', [Console::FG_GREEN]), + ]], + ])->setScreenWidth(200)->run() + ); + } } diff --git a/tests/framework/data/ActiveDataFilterTest.php b/tests/framework/data/ActiveDataFilterTest.php index 179f592..7e22c9d 100644 --- a/tests/framework/data/ActiveDataFilterTest.php +++ b/tests/framework/data/ActiveDataFilterTest.php @@ -134,6 +134,29 @@ class ActiveDataFilterTest extends TestCase ], ], ], + [ + [ + 'name' => 'NULL', + 'number' => 'NULL', + 'price' => 'NULL', + 'tags' => ['NULL'], + ], + [ + 'AND', + ['name' => ''], + ['number' => null], + ['price' => null], + ['tags' => [null]], + ], + ], + [ + [ + 'number' => [ + 'neq' => 'NULL' + ], + ], + ['!=', 'number', null], + ], ]; } diff --git a/tests/framework/data/ActiveDataProviderCloningTest.php b/tests/framework/data/ActiveDataProviderCloningTest.php index e4c7adc..1027baf 100644 --- a/tests/framework/data/ActiveDataProviderCloningTest.php +++ b/tests/framework/data/ActiveDataProviderCloningTest.php @@ -13,22 +13,18 @@ use yiiunit\TestCase; class ActiveDataProviderCloningTest extends TestCase { - - // Tests : - public function testClone() { $queryFirst = new Query(); - + $dataProviderFirst = new ActiveDataProvider([ 'query' => $queryFirst ]); - + $dataProviderSecond = clone $dataProviderFirst; - + $querySecond = $dataProviderSecond->query; - + $this->assertNotSame($querySecond, $queryFirst); } } - diff --git a/tests/framework/data/ArrayDataProviderTest.php b/tests/framework/data/ArrayDataProviderTest.php index 3d74535..a2da0e4 100644 --- a/tests/framework/data/ArrayDataProviderTest.php +++ b/tests/framework/data/ArrayDataProviderTest.php @@ -184,4 +184,30 @@ class ArrayDataProviderTest extends TestCase $dataProvider = new ArrayDataProvider(['allModels' => $mixedArray, 'pagination' => $pagination]); $this->assertEquals(['key1', 9], $dataProvider->getKeys()); } + + public function testSortFlags() + { + $simpleArray = [['sortField' => 1], ['sortField' => 2], ['sortField' => 11]]; + $dataProvider = new ArrayDataProvider( + [ + 'allModels' => $simpleArray, + 'sort' => [ + 'sortFlags' => SORT_STRING, + 'attributes' => [ + 'sort' => [ + 'asc' => ['sortField' => SORT_ASC], + 'desc' => ['sortField' => SORT_DESC], + 'label' => 'Sorting', + 'default' => 'asc', + ], + ], + 'defaultOrder' => [ + 'sort' => SORT_ASC, + ], + ], + ] + ); + $sortedArray = [['sortField' => 1], ['sortField' => 11], ['sortField' => 2]]; + $this->assertEquals($sortedArray, $dataProvider->getModels()); + } } diff --git a/tests/framework/data/DataFilterTest.php b/tests/framework/data/DataFilterTest.php index e29546e..2490853 100644 --- a/tests/framework/data/DataFilterTest.php +++ b/tests/framework/data/DataFilterTest.php @@ -225,6 +225,31 @@ class DataFilterTest extends TestCase true, [], ], + [ + [ + 'name' => [ + 'eq' => 'NULL', + ], + ], + true, + [], + ], + [ + [ + 'name' => 'NULL', + ], + true, + [], + ], + [ + [ + 'name' => [ + 'neq' => 'NULL', + ], + ], + true, + [], + ], ]; } @@ -363,6 +388,14 @@ class DataFilterTest extends TestCase 'datetime' => '2015-06-06 17:46:12', ], ], + [ + [ + 'name' => 'NULL', + ], + [ + 'name' => null, + ], + ], ]; } @@ -403,6 +436,15 @@ class DataFilterTest extends TestCase $this->assertEquals($expectedResult, $builder->normalize(false)); } + public function testNormalizeNonDefaultNull() + { + $builder = new DataFilter(); + $builder->nullValue = 'abcde'; + $builder->setSearchModel((new DynamicModel(['name' => null]))->addRule('name', 'string')); + $builder->filter = ['name' => 'abcde']; + $this->assertEquals(['name' => null], $builder->normalize(false)); + } + public function testSetupErrorMessages() { $builder = new DataFilter(); diff --git a/tests/framework/data/PaginationTest.php b/tests/framework/data/PaginationTest.php index 98bc9ec..5f2d348 100644 --- a/tests/framework/data/PaginationTest.php +++ b/tests/framework/data/PaginationTest.php @@ -8,6 +8,7 @@ namespace yiiunit\framework\data; use yii\data\Pagination; +use yii\web\Link; use yiiunit\TestCase; /** @@ -58,6 +59,13 @@ class PaginationTest extends TestCase '/index.php?r=item%2Flist&q=test&page=3&per-page=5', ['q' => 'test'], ], + [ + 1, + 10, + '/index.php?r=item%2Flist&page=2&per-page=10', + null, + true, + ], ]; } @@ -68,13 +76,14 @@ class PaginationTest extends TestCase * @param int $pageSize * @param string $expectedUrl * @param array $params + * @param bool $absolute */ - public function testCreateUrl($page, $pageSize, $expectedUrl, $params) + public function testCreateUrl($page, $pageSize, $expectedUrl, $params, $absolute = false) { $pagination = new Pagination(); $pagination->route = 'item/list'; $pagination->params = $params; - $this->assertEquals($expectedUrl, $pagination->createUrl($page, $pageSize)); + $this->assertEquals($expectedUrl, $pagination->createUrl($page, $pageSize, $absolute)); } /** @@ -105,4 +114,312 @@ class PaginationTest extends TestCase $pagination->setPage(999, false); $this->assertEquals(999, $pagination->getPage()); } + + public function dataProviderPageCount() + { + return [ + [0, 0, 0], + [0, 1, 1], + [-1, 0, 0], + [-1, 1, 1], + [1, -1, 0], + [1, 0, 0], + [1, 1, 1], + [10, 10, 1], + [10, 20, 2], + [2, 15, 8], + ]; + } + + /** + * @dataProvider dataProviderPageCount + * + * @param int $pageSize + * @param int $totalCount + * @param int $pageCount + */ + public function testPageCount($pageSize, $totalCount, $pageCount) + { + $pagination = new Pagination(); + $pagination->setPageSize($pageSize); + $pagination->totalCount = $totalCount; + + $this->assertEquals($pageCount, $pagination->getPageCount()); + } + + public function testGetDefaultPage() + { + $this->assertEquals(0, (new Pagination())->getPage()); + } + + public function dataProviderSetPage() + { + return [ + [null, false, 0, null], + [null, true, 0, null], + [0, false, 0, 0], + [0, true, 0, 0], + [-1, false, 0, 0], + [-1, true, 0, 0], + [1, false, 0, 1], + [1, true, 0, 0], + [2, false, 10, 2], + [2, true, 10, 0], + [2, false, 40, 2], + [2, true, 40, 1], + ]; + } + + /** + * @dataProvider dataProviderSetPage + * + * @param int|null $value + * @param bool $validate + * @param int $totalCount + * @param int $page + */ + public function testSetPage($value, $validate, $totalCount, $page) + { + $pagination = new Pagination(); + $pagination->totalCount = $totalCount; + $pagination->setPage($value, $validate); + + $this->assertEquals($page, $pagination->getPage()); + } + + public function dataProviderGetPageSize() + { + return [ + [[1, 50], 20], + [[], 20], + [[1], 20], + [['a' => 1, 50], 20], + [['a' => 1, 'b' => 50], 20], + [[2, 10], 10], + [[30, 100], 30], + ]; + } + + /** + * @dataProvider dataProviderGetPageSize + * + * @param array|bool $pageSizeLimit + * @param int $pageSize + */ + public function testGetPageSize($pageSizeLimit, $pageSize) + { + $pagination = new Pagination(); + $pagination->pageSizeLimit = $pageSizeLimit; + + $this->assertEquals($pageSize, $pagination->getPageSize()); + } + + public function dataProviderSetPageSize() + { + return [ + [null, false, false, 20], + [null, true, false, 20], + [null, false, [1, 50], 20], + [null, true, [1, 50], 20], + [1, false, false, 1], + [1, true, false, 1], + [1, false, [1, 50], 1], + [1, true, [1, 50], 1], + [10, false, [20, 50], 10], + [10, true, [20, 50], 20], + [40, false, [1, 20], 40], + [40, true, [1, 20], 20], + ]; + } + + /** + * @dataProvider dataProviderSetPageSize + * + * @param int|null $value + * @param bool $validate + * @param array|false $pageSizeLimit + * @param int $pageSize + */ + public function testSetPageSize($value, $validate, $pageSizeLimit, $pageSize) + { + $pagination = new Pagination(); + $pagination->pageSizeLimit = $pageSizeLimit; + $pagination->setPageSize($value, $validate); + + $this->assertEquals($pageSize, $pagination->getPageSize()); + } + + public function dataProviderGetOffset() + { + return [ + [0, 0, 0], + [0, 1, 0], + [1, 1, 1], + [1, 2, 2], + [10, 2, 20], + ]; + } + + /** + * @dataProvider dataProviderGetOffset + * + * @param int $pageSize + * @param int $page + * @param int $offset + */ + public function testGetOffset($pageSize, $page, $offset) + { + $pagination = new Pagination(); + $pagination->setPageSize($pageSize); + $pagination->setPage($page); + + $this->assertEquals($offset, $pagination->getOffset()); + } + + public function dataProviderGetLimit() + { + return [ + [0, -1], + [1, 1], + [2, 2], + ]; + } + + /** + * @dataProvider dataProviderGetLimit + * + * @param int $pageSize + * @param int $limit + */ + public function testGetLimit($pageSize, $limit) + { + $pagination = new Pagination(); + $pagination->setPageSize($pageSize); + + $this->assertEquals($limit, $pagination->getLimit()); + } + + public function dataProviderGetLinks() + { + return [ + [0, 0, 0, '/index.php?r=list&page=1&per-page=0', null, null, null, null], + [1, 0, 0, '/index.php?r=list&page=1&per-page=0', null, null, null, null], + [ + 0, + 0, + 1, + '/index.php?r=list&page=1&per-page=0', + '/index.php?r=list&page=1&per-page=0', + '/index.php?r=list&page=1&per-page=0', + null, + null, + ], + [ + 1, + 0, + 1, + '/index.php?r=list&page=1&per-page=0', + '/index.php?r=list&page=1&per-page=0', + '/index.php?r=list&page=1&per-page=0', + null, + null, + ], + [ + 0, + 1, + 1, + '/index.php?r=list&page=1&per-page=1', + '/index.php?r=list&page=1&per-page=1', + '/index.php?r=list&page=1&per-page=1', + null, + null, + ], + [ + 1, + 1, + 1, + '/index.php?r=list&page=1&per-page=1', + '/index.php?r=list&page=1&per-page=1', + '/index.php?r=list&page=1&per-page=1', + null, + null, + ], + [ + 0, + 5, + 10, + '/index.php?r=list&page=1&per-page=5', + '/index.php?r=list&page=1&per-page=5', + '/index.php?r=list&page=2&per-page=5', + null, + '/index.php?r=list&page=2&per-page=5', + ], + [ + 1, + 5, + 10, + '/index.php?r=list&page=2&per-page=5', + '/index.php?r=list&page=1&per-page=5', + '/index.php?r=list&page=2&per-page=5', + '/index.php?r=list&page=1&per-page=5', + null, + ], + [ + 1, + 5, + 15, + '/index.php?r=list&page=2&per-page=5', + '/index.php?r=list&page=1&per-page=5', + '/index.php?r=list&page=3&per-page=5', + '/index.php?r=list&page=1&per-page=5', + '/index.php?r=list&page=3&per-page=5', + ], + ]; + } + + /** + * @dataProvider dataProviderGetLinks + * + * @param int $page + * @param int $pageSize + * @param int $totalCount + * @param string $self + * @param string|null $first + * @param string|null $last + * @param string|null $prev + * @param string|null $next + */ + public function testGetLinks($page, $pageSize, $totalCount, $self, $first, $last, $prev, $next) + { + $pagination = new Pagination(); + $pagination->totalCount = $totalCount; + $pagination->route = 'list'; + $pagination->setPageSize($pageSize); + $pagination->setPage($page, true); + + $links = $pagination->getLinks(); + + $this->assertSame($self, $links[Link::REL_SELF]); + + if ($first) { + $this->assertSame($first, $links[Pagination::LINK_FIRST]); + } else { + $this->assertArrayNotHasKey(Pagination::LINK_FIRST, $links); + } + if ($last) { + $this->assertSame($last, $links[Pagination::LINK_LAST]); + } else { + $this->assertArrayNotHasKey(Pagination::LINK_LAST, $links); + } + if ($prev) { + $this->assertSame($prev, $links[Pagination::LINK_PREV]); + } else { + $this->assertArrayNotHasKey(Pagination::LINK_PREV, $links); + } + if ($next) { + $this->assertSame($next, $links[Pagination::LINK_NEXT]); + } else { + $this->assertArrayNotHasKey(Pagination::LINK_NEXT, $links); + } + } } diff --git a/tests/framework/db/ActiveQueryModelConnectionTest.php b/tests/framework/db/ActiveQueryModelConnectionTest.php new file mode 100644 index 0000000..fdef6d5 --- /dev/null +++ b/tests/framework/db/ActiveQueryModelConnectionTest.php @@ -0,0 +1,78 @@ +globalConnection = $this->getMockBuilder('yii\db\Connection')->getMock(); + $this->modelConnection = $this->getMockBuilder('yii\db\Connection')->getMock(); + + $this->mockApplication([ + 'components' => [ + 'db' => $this->globalConnection + ] + ]); + + ActiveRecord::$db = $this->modelConnection; + } + + private function prepareConnectionMock($connection) + { + $command = $this->getMockBuilder('yii\db\Command')->getMock(); + $command->method('queryOne')->willReturn(false); + $connection->method('createCommand')->willReturn($command); + $builder = $this->getMockBuilder('yii\db\QueryBuilder')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('getQueryBuilder')->willReturn($builder); + } + + public function testEnsureModelConnectionForOne() + { + $this->globalConnection->expects($this->never())->method('getQueryBuilder'); + $this->prepareConnectionMock($this->modelConnection); + + $query = new ActiveQuery(ActiveRecord::className()); + $query->one(); + } + + public function testEnsureGlobalConnectionForOne() + { + $this->modelConnection->expects($this->never())->method('getQueryBuilder'); + $this->prepareConnectionMock($this->globalConnection); + + $query = new ActiveQuery(DefaultActiveRecord::className()); + $query->one(); + } + + public function testEnsureModelConnectionForAll() + { + $this->globalConnection->expects($this->never())->method('getQueryBuilder'); + $this->prepareConnectionMock($this->modelConnection); + + $query = new ActiveQuery(ActiveRecord::className()); + $query->all(); + } + + public function testEnsureGlobalConnectionForAll() + { + $this->modelConnection->expects($this->never())->method('getQueryBuilder'); + $this->prepareConnectionMock($this->globalConnection); + + $query = new ActiveQuery(DefaultActiveRecord::className()); + $query->all(); + } +} diff --git a/tests/framework/db/ActiveQueryTest.php b/tests/framework/db/ActiveQueryTest.php index 5f85ebb..2fd9fde 100644 --- a/tests/framework/db/ActiveQueryTest.php +++ b/tests/framework/db/ActiveQueryTest.php @@ -136,6 +136,26 @@ abstract class ActiveQueryTest extends DatabaseTestCase ], $result->joinWith); } + public function testBuildJoinWithRemoveDuplicateJoinByTableName() + { + $query = new ActiveQuery(Customer::className()); + $query->innerJoinWith('orders') + ->joinWith('orders.orderItems'); + $this->invokeMethod($query, 'buildJoinWith'); + $this->assertEquals([ + [ + 'INNER JOIN', + 'order', + '{{customer}}.[[id]] = {{order}}.[[customer_id]]' + ], + [ + 'LEFT JOIN', + 'order_item', + '{{order}}.[[id]] = {{order_item}}.[[order_id]]' + ], + ], $query->join); + } + /** * @todo: tests for the regex inside getQueryTableName */ diff --git a/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index 142a63e..56f936a 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -564,6 +564,180 @@ abstract class ActiveRecordTest extends DatabaseTestCase /** * @depends testJoinWith */ + public function testJoinWithDuplicateSimple() + { + // left join and eager loading + $orders = Order::find() + ->innerJoinWith('customer') + ->joinWith('customer') + ->orderBy('customer.id DESC, order.id') + ->all(); + $this->assertCount(3, $orders); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertEquals(1, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + $this->assertTrue($orders[2]->isRelationPopulated('customer')); + } + + /** + * @depends testJoinWith + */ + public function testJoinWithDuplicateCallbackFiltering() + { + // inner join filtering and eager loading + $orders = Order::find() + ->innerJoinWith('customer') + ->joinWith([ + 'customer' => function ($query) { + $query->where('{{customer}}.[[id]]=2'); + }, + ])->orderBy('order.id')->all(); + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + } + + /** + * @depends testJoinWith + */ + public function testJoinWithDuplicateCallbackFilteringConditionsOnPrimary() + { + // inner join filtering, eager loading, conditions on both primary and relation + $orders = Order::find() + ->innerJoinWith('customer') + ->joinWith([ + 'customer' => function ($query) { + $query->where(['{{customer}}.[[id]]' => 2]); + }, + ])->where(['order.id' => [1, 2]])->orderBy('order.id')->all(); + $this->assertCount(1, $orders); + $this->assertEquals(2, $orders[0]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + } + + /** + * @depends testJoinWith + */ + public function testJoinWithDuplicateWithSubRelation() + { + // join with sub-relation + $orders = Order::find() + ->innerJoinWith('items') + ->joinWith([ + 'items.category' => function ($q) { + $q->where('{{category}}.[[id]] = 2'); + }, + ])->orderBy('order.id')->all(); + $this->assertCount(1, $orders); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + $this->assertEquals(2, $orders[0]->id); + $this->assertCount(3, $orders[0]->items); + $this->assertTrue($orders[0]->items[0]->isRelationPopulated('category')); + $this->assertEquals(2, $orders[0]->items[0]->category->id); + } + + /** + * @depends testJoinWith + */ + public function testJoinWithDuplicateTableAlias1() + { + // join with table alias + $orders = Order::find() + ->innerJoinWith('customer') + ->joinWith([ + 'customer' => function ($q) { + $q->from('customer c'); + }, + ])->orderBy('c.id DESC, order.id')->all(); + $this->assertCount(3, $orders); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertEquals(1, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + $this->assertTrue($orders[2]->isRelationPopulated('customer')); + } + + /** + * @depends testJoinWith + */ + public function testJoinWithDuplicateTableAlias2() + { + // join with table alias + $orders = Order::find() + ->innerJoinWith('customer') + ->joinWith('customer as c') + ->orderBy('c.id DESC, order.id') + ->all(); + $this->assertCount(3, $orders); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertEquals(1, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + $this->assertTrue($orders[2]->isRelationPopulated('customer')); + } + + /** + * @depends testJoinWith + */ + public function testJoinWithDuplicateTableAliasSubRelation() + { + // join with table alias sub-relation + $orders = Order::find() + ->innerJoinWith([ + 'items as t' => function ($q) { + $q->orderBy('t.id'); + }, + ]) + ->joinWith([ + 'items.category as c' => function ($q) { + $q->where('{{c}}.[[id]] = 2'); + }, + ])->orderBy('order.id')->all(); + $this->assertCount(1, $orders); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + $this->assertEquals(2, $orders[0]->id); + $this->assertCount(3, $orders[0]->items); + $this->assertTrue($orders[0]->items[0]->isRelationPopulated('category')); + $this->assertEquals(2, $orders[0]->items[0]->category->id); + } + + /** + * @depends testJoinWith + */ + public function testJoinWithDuplicateSubRelationCalledInsideClosure() + { + // join with sub-relation called inside Closure + $orders = Order::find() + ->innerJoinWith('items') + ->joinWith([ + 'items' => function ($q) { + $q->orderBy('item.id'); + $q->joinWith([ + 'category' => function ($q) { + $q->where('{{category}}.[[id]] = 2'); + }, + ]); + }, + ]) + ->orderBy('order.id') + ->all(); + $this->assertCount(1, $orders); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + $this->assertEquals(2, $orders[0]->id); + $this->assertCount(3, $orders[0]->items); + $this->assertTrue($orders[0]->items[0]->isRelationPopulated('category')); + $this->assertEquals(2, $orders[0]->items[0]->category->id); + } + + /** + * @depends testJoinWith + */ public function testJoinWithAndScope() { // hasOne inner join @@ -766,7 +940,7 @@ abstract class ActiveRecordTest extends DatabaseTestCase /** @var $query ActiveQuery */ $query = Order::find()->joinWith(['customer c']); if ($aliasMethod === 'explicit') { - $count = $query->count('c.id'); + $count = $query->count('[[c.id]]'); } elseif ($aliasMethod === 'querysyntax') { $count = $query->count('{{@customer}}.id'); } elseif ($aliasMethod === 'applyAlias') { @@ -846,12 +1020,12 @@ abstract class ActiveRecordTest extends DatabaseTestCase $query = Order::find() ->joinWith([ 'itemsIndexed books' => function ($q) { - $q->onCondition('books.category_id = 1'); + $q->onCondition('[[books.category_id]] = 1'); }, ], false) ->joinWith([ 'itemsIndexed movies' => function ($q) { - $q->onCondition('movies.category_id = 2'); + $q->onCondition('[[movies.category_id]] = 2'); }, ], false) ->where(['movies.name' => 'Toy Story']); @@ -863,12 +1037,12 @@ abstract class ActiveRecordTest extends DatabaseTestCase $query = Order::find() ->joinWith([ 'itemsIndexed books' => function ($q) { - $q->onCondition('books.category_id = 1'); + $q->onCondition('[[books.category_id]] = 1'); }, ], false) ->joinWith([ 'itemsIndexed movies' => function ($q) { - $q->onCondition('movies.category_id = 2'); + $q->onCondition('[[movies.category_id]] = 2'); }, ], true) ->where(['movies.name' => 'Toy Story']); @@ -881,15 +1055,15 @@ abstract class ActiveRecordTest extends DatabaseTestCase $query = Order::find() ->joinWith([ 'itemsIndexed books' => function ($q) { - $q->onCondition('books.category_id = 1'); + $q->onCondition('[[books.category_id]] = 1'); }, ], true) ->joinWith([ 'itemsIndexed movies' => function ($q) { - $q->onCondition('movies.category_id = 2'); + $q->onCondition('[[movies.category_id]] = 2'); }, ], false) - ->where(['movies.name' => 'Toy Story']); + ->where(['[[movies.name]]' => 'Toy Story']); $orders = $query->all(); $this->assertCount(1, $orders, $query->createCommand()->rawSql . print_r($orders, true)); $this->assertEquals(2, $orders[0]->id); @@ -1719,24 +1893,24 @@ abstract class ActiveRecordTest extends DatabaseTestCase public function illegalValuesForFindByCondition() { return [ - [Customer::className(), ['id' => ['`id`=`id` and 1' => 1]]], - [Customer::className(), ['id' => [ + [Customer::className(), [['`id`=`id` and 1' => 1]]], + [Customer::className(), [[ 'legal' => 1, '`id`=`id` and 1' => 1, ]]], - [Customer::className(), ['id' => [ + [Customer::className(), [[ 'nested_illegal' => [ 'false or 1=' => 1 ] ]]], [Customer::className(), [['true--' => 1]]], - [CustomerWithAlias::className(), ['csr.id' => ['`csr`.`id`=`csr`.`id` and 1' => 1]]], - [CustomerWithAlias::className(), ['csr.id' => [ + [CustomerWithAlias::className(), [['`csr`.`id`=`csr`.`id` and 1' => 1]]], + [CustomerWithAlias::className(), [[ 'legal' => 1, '`csr`.`id`=`csr`.`id` and 1' => 1, ]]], - [CustomerWithAlias::className(), ['csr.id' => [ + [CustomerWithAlias::className(), [[ 'nested_illegal' => [ 'false or 1=' => 1 ] @@ -1906,4 +2080,40 @@ abstract class ActiveRecordTest extends DatabaseTestCase $this->assertEquals(['1', '01', '001', '001', '2', '2b', '2b', '02'], $alphaIdentifiers); } + + /** + * @see https://github.com/yiisoft/yii2/issues/16492 + */ + public function testEagerLoadingWithTypeCastedCompositeIdentifier() + { + $aggregation = Order::find()->joinWith('quantityOrderItems', true)->all(); + foreach ($aggregation as $item) { + if ($item->id == 1) { + $this->assertEquals(1, $item->quantityOrderItems[0]->order_id); + } elseif ($item->id != 1) { + $this->assertCount(0, $item->quantityOrderItems); + } + } + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18525 + */ + public function testHasManyWithIndexBy() + { + $category = Category::find()->joinWith('items')->indexBy('items.0.name'); + $this->assertEquals(['Agile Web Application Development with Yii1.1 and PHP5', 'Ice Age'], array_keys($category->all())); + + $category = Category::find()->select([Category::tableName() . '.*'])->joinWith('items')->indexBy('items.0.name'); + $this->assertEquals(['Agile Web Application Development with Yii1.1 and PHP5', 'Ice Age'], array_keys($category->all())); + + $category = Category::find()->select([Category::tableName() . '.*'])->joinWith('items')->indexBy('name'); + $this->assertEquals(['Books', 'Movies'], array_keys($category->all())); + + $category = Category::find()->joinWith('items')->indexBy('item.name'); + $this->assertEquals([''], array_keys($category->all())); + + $category = Category::find()->select([Category::tableName() . '.name'])->joinWith('items')->indexBy('id'); + $this->assertEquals([1, 2], array_keys($category->all())); + } } diff --git a/tests/framework/db/CommandTest.php b/tests/framework/db/CommandTest.php index 39379ce..7cee84b 100644 --- a/tests/framework/db/CommandTest.php +++ b/tests/framework/db/CommandTest.php @@ -8,7 +8,7 @@ namespace yiiunit\framework\db; use ArrayObject; -use yii\caching\FileCache; +use yii\caching\ArrayCache; use yii\db\Connection; use yii\db\DataReader; use yii\db\Exception; @@ -178,26 +178,30 @@ SQL; $command = $db->createCommand($sql); $intCol = 123; $charCol = str_repeat('abc', 33) . 'x'; // a 100 char string - $boolCol = false; $command->bindParam(':int_col', $intCol, \PDO::PARAM_INT); $command->bindParam(':char_col', $charCol); - $command->bindParam(':bool_col', $boolCol, \PDO::PARAM_BOOL); if ($this->driverName === 'oci') { // can't bind floats without support from a custom PDO driver $floatCol = 2; $numericCol = 3; // can't use blobs without support from a custom PDO driver $blobCol = null; + // You can create a table with a column of datatype CHAR(1) and store either “Y” or “N” in that column + // to indicate TRUE or FALSE. + $boolCol = '0'; $command->bindParam(':float_col', $floatCol, \PDO::PARAM_INT); $command->bindParam(':numeric_col', $numericCol, \PDO::PARAM_INT); $command->bindParam(':blob_col', $blobCol); + $command->bindParam(':bool_col', $boolCol, \PDO::PARAM_BOOL); } else { $floatCol = 1.23; $numericCol = '1.23'; $blobCol = "\x10\x11\x12"; + $boolCol = false; $command->bindParam(':float_col', $floatCol); $command->bindParam(':numeric_col', $numericCol); $command->bindParam(':blob_col', $blobCol); + $command->bindParam(':bool_col', $boolCol, \PDO::PARAM_BOOL); } $this->assertEquals(1, $command->execute()); @@ -655,6 +659,9 @@ SQL; break; case 'sqlsrv': $expression = 'YEAR(GETDATE())'; + break; + case 'oci': + $expression = 'EXTRACT(YEAR FROM sysdate)'; } $command = $db->createCommand(); @@ -1253,7 +1260,7 @@ SQL; { $db = $this->getConnection(); $db->enableQueryCache = true; - $db->queryCache = new FileCache(['cachePath' => '@yiiunit/runtime/cache']); + $db->queryCache = new ArrayCache(); $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); $this->assertEquals('user1', $command->bindValue(':id', 1)->queryScalar()); @@ -1392,8 +1399,13 @@ SQL; [ 'SELECT * FROM customer WHERE id IN (:ids)', [':ids' => new Expression(implode(', ', [1, 2]))], - 'SELECT * FROM customer WHERE id IN (1, 2)', + 'SELECT * FROM customer WHERE id IN (\'1, 2\')', ], + [ + 'SELECT NOW() = :now', + [':now' => new Expression('NOW()')], + 'SELECT NOW() = \'NOW()\'', + ] ]; } diff --git a/tests/framework/db/ConnectionTest.php b/tests/framework/db/ConnectionTest.php index b046492..be5ed4e 100644 --- a/tests/framework/db/ConnectionTest.php +++ b/tests/framework/db/ConnectionTest.php @@ -85,6 +85,7 @@ abstract class ConnectionTest extends DatabaseTestCase $this->assertEquals('`schema`.`table`', $connection->quoteTableName('`schema`.`table`')); $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + $this->assertEquals('`table(0)`', $connection->quoteTableName('table(0)')); } public function testQuoteColumnName() @@ -136,7 +137,12 @@ abstract class ConnectionTest extends DatabaseTestCase $this->assertFalse($transaction->isActive); $this->assertNull($connection->transaction); - $this->assertEquals(0, $connection->createCommand("SELECT COUNT(*) FROM profile WHERE description = 'test transaction';")->queryScalar()); + $this->assertEquals( + 0, + $connection->createCommand( + "SELECT COUNT(*) FROM {{profile}} WHERE [[description]] = 'test transaction'" + )->queryScalar() + ); $transaction = $connection->beginTransaction(); $connection->createCommand()->insert('profile', ['description' => 'test transaction'])->execute(); @@ -144,7 +150,12 @@ abstract class ConnectionTest extends DatabaseTestCase $this->assertFalse($transaction->isActive); $this->assertNull($connection->transaction); - $this->assertEquals(1, $connection->createCommand("SELECT COUNT(*) FROM profile WHERE description = 'test transaction';")->queryScalar()); + $this->assertEquals( + 1, + $connection->createCommand( + "SELECT COUNT(*) FROM {{profile}} WHERE [[description]] = 'test transaction'" + )->queryScalar() + ); } public function testTransactionIsolation() @@ -192,7 +203,10 @@ abstract class ConnectionTest extends DatabaseTestCase $this->assertTrue($result, 'transaction shortcut valid value should be returned from callback'); - $profilesCount = $connection->createCommand("SELECT COUNT(*) FROM profile WHERE description = 'test transaction shortcut';")->queryScalar(); + $profilesCount = $connection->createCommand( + "SELECT COUNT(*) FROM {{profile}} WHERE [[description]] = 'test transaction shortcut'" + )->queryScalar(); + $this->assertEquals(1, $profilesCount, 'profile should be inserted in transaction shortcut'); } @@ -259,7 +273,7 @@ abstract class ConnectionTest extends DatabaseTestCase $this->assertNotNull($connection->getTableSchema('qlog1', true)); \Yii::getLogger()->messages = []; - $connection->createCommand('SELECT * FROM qlog1')->queryAll(); + $connection->createCommand('SELECT * FROM {{qlog1}}')->queryAll(); $this->assertCount(3, \Yii::getLogger()->messages); // profiling only @@ -272,7 +286,7 @@ abstract class ConnectionTest extends DatabaseTestCase $this->assertNotNull($connection->getTableSchema('qlog2', true)); \Yii::getLogger()->messages = []; - $connection->createCommand('SELECT * FROM qlog2')->queryAll(); + $connection->createCommand('SELECT * FROM {{qlog2}}')->queryAll(); $this->assertCount(2, \Yii::getLogger()->messages); // logging only @@ -285,7 +299,7 @@ abstract class ConnectionTest extends DatabaseTestCase $this->assertNotNull($connection->getTableSchema('qlog3', true)); \Yii::getLogger()->messages = []; - $connection->createCommand('SELECT * FROM qlog3')->queryAll(); + $connection->createCommand('SELECT * FROM {{qlog3}}')->queryAll(); $this->assertCount(1, \Yii::getLogger()->messages); // disabled @@ -296,7 +310,7 @@ abstract class ConnectionTest extends DatabaseTestCase $connection->createCommand()->createTable('qlog4', ['id' => 'pk'])->execute(); $this->assertNotNull($connection->getTableSchema('qlog4', true)); $this->assertCount(0, \Yii::getLogger()->messages); - $connection->createCommand('SELECT * FROM qlog4')->queryAll(); + $connection->createCommand('SELECT * FROM {{qlog4}}')->queryAll(); $this->assertCount(0, \Yii::getLogger()->messages); } diff --git a/tests/framework/db/QueryBuilderTest.php b/tests/framework/db/QueryBuilderTest.php index c970505..3f0fd80 100644 --- a/tests/framework/db/QueryBuilderTest.php +++ b/tests/framework/db/QueryBuilderTest.php @@ -9,6 +9,8 @@ namespace yiiunit\framework\db; use yii\db\ColumnSchemaBuilder; use yii\db\conditions\BetweenColumnsCondition; +use yii\db\conditions\LikeCondition; +use yii\db\conditions\InCondition; use yii\db\cubrid\QueryBuilder as CubridQueryBuilder; use yii\db\Expression; use yii\db\mssql\QueryBuilder as MssqlQueryBuilder; @@ -82,7 +84,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->bigInteger(), [ 'mysql' => 'bigint(20)', - 'postgres' => 'bigint', + 'pgsql' => 'bigint', 'sqlite' => 'bigint', 'oci' => 'NUMBER(20)', 'sqlsrv' => 'bigint', @@ -94,7 +96,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->bigInteger()->notNull(), [ 'mysql' => 'bigint(20) NOT NULL', - 'postgres' => 'bigint NOT NULL', + 'pgsql' => 'bigint NOT NULL', 'sqlite' => 'bigint NOT NULL', 'oci' => 'NUMBER(20) NOT NULL', 'sqlsrv' => 'bigint NOT NULL', @@ -106,7 +108,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->bigInteger()->check('value > 5'), [ 'mysql' => 'bigint(20) CHECK (value > 5)', - 'postgres' => 'bigint CHECK (value > 5)', + 'pgsql' => 'bigint CHECK (value > 5)', 'sqlite' => 'bigint CHECK (value > 5)', 'oci' => 'NUMBER(20) CHECK (value > 5)', 'sqlsrv' => 'bigint CHECK (value > 5)', @@ -118,7 +120,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->bigInteger(8), [ 'mysql' => 'bigint(8)', - 'postgres' => 'bigint', + 'pgsql' => 'bigint', 'sqlite' => 'bigint', 'oci' => 'NUMBER(8)', 'sqlsrv' => 'bigint', @@ -130,7 +132,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->bigInteger(8)->check('value > 5'), [ 'mysql' => 'bigint(8) CHECK (value > 5)', - 'postgres' => 'bigint CHECK (value > 5)', + 'pgsql' => 'bigint CHECK (value > 5)', 'sqlite' => 'bigint CHECK (value > 5)', 'oci' => 'NUMBER(8) CHECK (value > 5)', 'sqlsrv' => 'bigint CHECK (value > 5)', @@ -142,7 +144,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->bigPrimaryKey(), [ 'mysql' => 'bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY', - 'postgres' => 'bigserial NOT NULL PRIMARY KEY', + 'pgsql' => 'bigserial NOT NULL PRIMARY KEY', 'sqlite' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', ], ], @@ -151,7 +153,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->binary(), [ 'mysql' => 'blob', - 'postgres' => 'bytea', + 'pgsql' => 'bytea', 'sqlite' => 'blob', 'oci' => 'BLOB', 'sqlsrv' => 'varbinary(max)', @@ -173,7 +175,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->boolean(), [ 'mysql' => 'tinyint(1)', - 'postgres' => 'boolean', + 'pgsql' => 'boolean', 'sqlite' => 'boolean', 'oci' => 'NUMBER(1)', 'sqlsrv' => 'bit', @@ -194,7 +196,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->char()->notNull(), [ 'mysql' => 'char(1) NOT NULL', - 'postgres' => 'char(1) NOT NULL', + 'pgsql' => 'char(1) NOT NULL', 'sqlite' => 'char(1) NOT NULL', 'oci' => 'CHAR(1) NOT NULL', 'cubrid' => 'char(1) NOT NULL', @@ -214,7 +216,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->char(6), [ 'mysql' => 'char(6)', - 'postgres' => 'char(6)', + 'pgsql' => 'char(6)', 'sqlite' => 'char(6)', 'oci' => 'CHAR(6)', 'cubrid' => 'char(6)', @@ -225,7 +227,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->char(), [ 'mysql' => 'char(1)', - 'postgres' => 'char(1)', + 'pgsql' => 'char(1)', 'sqlite' => 'char(1)', 'oci' => 'CHAR(1)', 'cubrid' => 'char(1)', @@ -236,7 +238,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase // $this->date()->check("value BETWEEN '2011-01-01' AND '2013-01-01'"), // [ // 'mysql' => , - // 'postgres' => , + // 'pgsql' => , // 'sqlite' => , // 'sqlsrv' => , // 'cubrid' => , @@ -247,7 +249,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->date()->notNull(), [ 'mysql' => 'date NOT NULL', - 'postgres' => 'date NOT NULL', + 'pgsql' => 'date NOT NULL', 'sqlite' => 'date NOT NULL', 'oci' => 'DATE NOT NULL', 'sqlsrv' => 'date NOT NULL', @@ -259,7 +261,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->date(), [ 'mysql' => 'date', - 'postgres' => 'date', + 'pgsql' => 'date', 'sqlite' => 'date', 'oci' => 'DATE', 'sqlsrv' => 'date', @@ -271,7 +273,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase // $this->dateTime()->check("value BETWEEN '2011-01-01' AND '2013-01-01'"), // [ // 'mysql' => , - // 'postgres' => , + // 'pgsql' => , // 'sqlite' => , // 'sqlsrv' => , // 'cubrid' => , @@ -281,7 +283,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_DATETIME . ' NOT NULL', $this->dateTime()->notNull(), [ - 'postgres' => 'timestamp(0) NOT NULL', + 'pgsql' => 'timestamp(0) NOT NULL', 'sqlite' => 'datetime NOT NULL', 'oci' => 'TIMESTAMP NOT NULL', 'sqlsrv' => 'datetime NOT NULL', @@ -292,7 +294,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_DATETIME, $this->dateTime(), [ - 'postgres' => 'timestamp(0)', + 'pgsql' => 'timestamp(0)', 'sqlite' => 'datetime', 'oci' => 'TIMESTAMP', 'sqlsrv' => 'datetime', @@ -304,7 +306,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->decimal()->check('value > 5.6'), [ 'mysql' => 'decimal(10,0) CHECK (value > 5.6)', - 'postgres' => 'numeric(10,0) CHECK (value > 5.6)', + 'pgsql' => 'numeric(10,0) CHECK (value > 5.6)', 'sqlite' => 'decimal(10,0) CHECK (value > 5.6)', 'oci' => 'NUMBER CHECK (value > 5.6)', 'sqlsrv' => 'decimal(18,0) CHECK (value > 5.6)', @@ -316,7 +318,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->decimal()->notNull(), [ 'mysql' => 'decimal(10,0) NOT NULL', - 'postgres' => 'numeric(10,0) NOT NULL', + 'pgsql' => 'numeric(10,0) NOT NULL', 'sqlite' => 'decimal(10,0) NOT NULL', 'oci' => 'NUMBER NOT NULL', 'sqlsrv' => 'decimal(18,0) NOT NULL', @@ -328,7 +330,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->decimal(12, 4)->check('value > 5.6'), [ 'mysql' => 'decimal(12,4) CHECK (value > 5.6)', - 'postgres' => 'numeric(12,4) CHECK (value > 5.6)', + 'pgsql' => 'numeric(12,4) CHECK (value > 5.6)', 'sqlite' => 'decimal(12,4) CHECK (value > 5.6)', 'oci' => 'NUMBER CHECK (value > 5.6)', 'sqlsrv' => 'decimal(12,4) CHECK (value > 5.6)', @@ -340,7 +342,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->decimal(12, 4), [ 'mysql' => 'decimal(12,4)', - 'postgres' => 'numeric(12,4)', + 'pgsql' => 'numeric(12,4)', 'sqlite' => 'decimal(12,4)', 'oci' => 'NUMBER', 'sqlsrv' => 'decimal(12,4)', @@ -352,7 +354,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->decimal(), [ 'mysql' => 'decimal(10,0)', - 'postgres' => 'numeric(10,0)', + 'pgsql' => 'numeric(10,0)', 'sqlite' => 'decimal(10,0)', 'oci' => 'NUMBER', 'sqlsrv' => 'decimal(18,0)', @@ -364,7 +366,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->double()->check('value > 5.6'), [ 'mysql' => 'double CHECK (value > 5.6)', - 'postgres' => 'double precision CHECK (value > 5.6)', + 'pgsql' => 'double precision CHECK (value > 5.6)', 'sqlite' => 'double CHECK (value > 5.6)', 'oci' => 'NUMBER CHECK (value > 5.6)', 'sqlsrv' => 'float CHECK (value > 5.6)', @@ -376,7 +378,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->double()->notNull(), [ 'mysql' => 'double NOT NULL', - 'postgres' => 'double precision NOT NULL', + 'pgsql' => 'double precision NOT NULL', 'sqlite' => 'double NOT NULL', 'oci' => 'NUMBER NOT NULL', 'sqlsrv' => 'float NOT NULL', @@ -388,7 +390,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->double(16)->check('value > 5.6'), [ 'mysql' => 'double CHECK (value > 5.6)', - 'postgres' => 'double precision CHECK (value > 5.6)', + 'pgsql' => 'double precision CHECK (value > 5.6)', 'sqlite' => 'double CHECK (value > 5.6)', 'oci' => 'NUMBER CHECK (value > 5.6)', 'sqlsrv' => 'float CHECK (value > 5.6)', @@ -411,7 +413,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->double(), [ 'mysql' => 'double', - 'postgres' => 'double precision', + 'pgsql' => 'double precision', 'sqlite' => 'double', 'oci' => 'NUMBER', 'sqlsrv' => 'float', @@ -423,7 +425,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->float()->check('value > 5.6'), [ 'mysql' => 'float CHECK (value > 5.6)', - 'postgres' => 'double precision CHECK (value > 5.6)', + 'pgsql' => 'double precision CHECK (value > 5.6)', 'sqlite' => 'float CHECK (value > 5.6)', 'oci' => 'NUMBER CHECK (value > 5.6)', 'sqlsrv' => 'float CHECK (value > 5.6)', @@ -435,7 +437,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->float()->notNull(), [ 'mysql' => 'float NOT NULL', - 'postgres' => 'double precision NOT NULL', + 'pgsql' => 'double precision NOT NULL', 'sqlite' => 'float NOT NULL', 'oci' => 'NUMBER NOT NULL', 'sqlsrv' => 'float NOT NULL', @@ -447,7 +449,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->float(16)->check('value > 5.6'), [ 'mysql' => 'float CHECK (value > 5.6)', - 'postgres' => 'double precision CHECK (value > 5.6)', + 'pgsql' => 'double precision CHECK (value > 5.6)', 'sqlite' => 'float CHECK (value > 5.6)', 'oci' => 'NUMBER CHECK (value > 5.6)', 'sqlsrv' => 'float CHECK (value > 5.6)', @@ -470,7 +472,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->float(), [ 'mysql' => 'float', - 'postgres' => 'double precision', + 'pgsql' => 'double precision', 'sqlite' => 'float', 'oci' => 'NUMBER', 'sqlsrv' => 'float', @@ -482,7 +484,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->integer()->check('value > 5'), [ 'mysql' => 'int(11) CHECK (value > 5)', - 'postgres' => 'integer CHECK (value > 5)', + 'pgsql' => 'integer CHECK (value > 5)', 'sqlite' => 'integer CHECK (value > 5)', 'oci' => 'NUMBER(10) CHECK (value > 5)', 'sqlsrv' => 'int CHECK (value > 5)', @@ -494,7 +496,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->integer()->notNull(), [ 'mysql' => 'int(11) NOT NULL', - 'postgres' => 'integer NOT NULL', + 'pgsql' => 'integer NOT NULL', 'sqlite' => 'integer NOT NULL', 'oci' => 'NUMBER(10) NOT NULL', 'sqlsrv' => 'int NOT NULL', @@ -506,7 +508,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->integer(8)->check('value > 5'), [ 'mysql' => 'int(8) CHECK (value > 5)', - 'postgres' => 'integer CHECK (value > 5)', + 'pgsql' => 'integer CHECK (value > 5)', 'sqlite' => 'integer CHECK (value > 5)', 'oci' => 'NUMBER(8) CHECK (value > 5)', 'sqlsrv' => 'int CHECK (value > 5)', @@ -518,7 +520,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->integer(8), [ 'mysql' => 'int(8)', - 'postgres' => 'integer', + 'pgsql' => 'integer', 'sqlite' => 'integer', 'oci' => 'NUMBER(8)', 'sqlsrv' => 'int', @@ -530,7 +532,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->integer(), [ 'mysql' => 'int(11)', - 'postgres' => 'integer', + 'pgsql' => 'integer', 'sqlite' => 'integer', 'oci' => 'NUMBER(10)', 'sqlsrv' => 'int', @@ -542,7 +544,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->money()->check('value > 0.0'), [ 'mysql' => 'decimal(19,4) CHECK (value > 0.0)', - 'postgres' => 'numeric(19,4) CHECK (value > 0.0)', + 'pgsql' => 'numeric(19,4) CHECK (value > 0.0)', 'sqlite' => 'decimal(19,4) CHECK (value > 0.0)', 'oci' => 'NUMBER(19,4) CHECK (value > 0.0)', 'sqlsrv' => 'decimal(19,4) CHECK (value > 0.0)', @@ -554,7 +556,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->money()->notNull(), [ 'mysql' => 'decimal(19,4) NOT NULL', - 'postgres' => 'numeric(19,4) NOT NULL', + 'pgsql' => 'numeric(19,4) NOT NULL', 'sqlite' => 'decimal(19,4) NOT NULL', 'oci' => 'NUMBER(19,4) NOT NULL', 'sqlsrv' => 'decimal(19,4) NOT NULL', @@ -566,7 +568,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->money(16, 2)->check('value > 0.0'), [ 'mysql' => 'decimal(16,2) CHECK (value > 0.0)', - 'postgres' => 'numeric(16,2) CHECK (value > 0.0)', + 'pgsql' => 'numeric(16,2) CHECK (value > 0.0)', 'sqlite' => 'decimal(16,2) CHECK (value > 0.0)', 'oci' => 'NUMBER(16,2) CHECK (value > 0.0)', 'sqlsrv' => 'decimal(16,2) CHECK (value > 0.0)', @@ -578,7 +580,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->money(16, 2), [ 'mysql' => 'decimal(16,2)', - 'postgres' => 'numeric(16,2)', + 'pgsql' => 'numeric(16,2)', 'sqlite' => 'decimal(16,2)', 'oci' => 'NUMBER(16,2)', 'sqlsrv' => 'decimal(16,2)', @@ -590,7 +592,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->money(), [ 'mysql' => 'decimal(19,4)', - 'postgres' => 'numeric(19,4)', + 'pgsql' => 'numeric(19,4)', 'sqlite' => 'decimal(19,4)', 'oci' => 'NUMBER(19,4)', 'sqlsrv' => 'decimal(19,4)', @@ -602,7 +604,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->primaryKey()->check('value > 5'), [ 'mysql' => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)', - 'postgres' => 'serial NOT NULL PRIMARY KEY CHECK (value > 5)', + 'pgsql' => 'serial NOT NULL PRIMARY KEY CHECK (value > 5)', 'sqlite' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)', 'oci' => 'NUMBER(10) NOT NULL PRIMARY KEY CHECK (value > 5)', 'sqlsrv' => 'int IDENTITY PRIMARY KEY CHECK (value > 5)', @@ -630,7 +632,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->primaryKey(), [ 'mysql' => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY', - 'postgres' => 'serial NOT NULL PRIMARY KEY', + 'pgsql' => 'serial NOT NULL PRIMARY KEY', 'sqlite' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', 'oci' => 'NUMBER(10) NOT NULL PRIMARY KEY', 'sqlsrv' => 'int IDENTITY PRIMARY KEY', @@ -642,7 +644,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->tinyInteger(2), [ 'mysql' => 'tinyint(2)', - 'postgres' => 'smallint', + 'pgsql' => 'smallint', 'sqlite' => 'tinyint', 'oci' => 'NUMBER(2)', 'sqlsrv' => 'tinyint', @@ -654,17 +656,16 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->tinyInteger()->unsigned(), [ 'mysql' => 'tinyint(3) UNSIGNED', - 'postgres' => 'smallint UNSIGNED', 'sqlite' => 'tinyint UNSIGNED', 'cubrid' => 'smallint UNSIGNED', - ], + ] ], [ Schema::TYPE_TINYINT, $this->tinyInteger(), [ 'mysql' => 'tinyint(3)', - 'postgres' => 'smallint', + 'pgsql' => 'smallint', 'sqlite' => 'tinyint', 'oci' => 'NUMBER(3)', 'sqlsrv' => 'tinyint', @@ -676,7 +677,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->smallInteger(8), [ 'mysql' => 'smallint(8)', - 'postgres' => 'smallint', + 'pgsql' => 'smallint', 'sqlite' => 'smallint', 'oci' => 'NUMBER(8)', 'sqlsrv' => 'smallint', @@ -688,7 +689,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->smallInteger(), [ 'mysql' => 'smallint(6)', - 'postgres' => 'smallint', + 'pgsql' => 'smallint', 'sqlite' => 'smallint', 'oci' => 'NUMBER(5)', 'sqlsrv' => 'smallint', @@ -709,7 +710,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_STRING . ' CHECK (value LIKE \'test%\')', $this->string()->check('value LIKE \'test%\''), [ - 'postgres' => 'varchar(255) CHECK (value LIKE \'test%\')', + 'pgsql' => 'varchar(255) CHECK (value LIKE \'test%\')', 'oci' => 'VARCHAR2(255) CHECK (value LIKE \'test%\')', ], ], @@ -718,7 +719,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->string()->notNull(), [ 'mysql' => 'varchar(255) NOT NULL', - 'postgres' => 'varchar(255) NOT NULL', + 'pgsql' => 'varchar(255) NOT NULL', 'sqlite' => 'varchar(255) NOT NULL', 'oci' => 'VARCHAR2(255) NOT NULL', 'sqlsrv' => 'nvarchar(255) NOT NULL', @@ -739,7 +740,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_STRING . '(32) CHECK (value LIKE \'test%\')', $this->string(32)->check('value LIKE \'test%\''), [ - 'postgres' => 'varchar(32) CHECK (value LIKE \'test%\')', + 'pgsql' => 'varchar(32) CHECK (value LIKE \'test%\')', 'oci' => 'VARCHAR2(32) CHECK (value LIKE \'test%\')', ], ], @@ -748,7 +749,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->string(32), [ 'mysql' => 'varchar(32)', - 'postgres' => 'varchar(32)', + 'pgsql' => 'varchar(32)', 'sqlite' => 'varchar(32)', 'oci' => 'VARCHAR2(32)', 'sqlsrv' => 'nvarchar(32)', @@ -760,7 +761,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->string(), [ 'mysql' => 'varchar(255)', - 'postgres' => 'varchar(255)', + 'pgsql' => 'varchar(255)', 'sqlite' => 'varchar(255)', 'oci' => 'VARCHAR2(255)', 'sqlsrv' => 'nvarchar(255)', @@ -781,7 +782,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_TEXT . ' CHECK (value LIKE \'test%\')', $this->text()->check('value LIKE \'test%\''), [ - 'postgres' => 'text CHECK (value LIKE \'test%\')', + 'pgsql' => 'text CHECK (value LIKE \'test%\')', 'oci' => 'CLOB CHECK (value LIKE \'test%\')', ], ], @@ -790,7 +791,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->text()->notNull(), [ 'mysql' => 'text NOT NULL', - 'postgres' => 'text NOT NULL', + 'pgsql' => 'text NOT NULL', 'sqlite' => 'text NOT NULL', 'oci' => 'CLOB NOT NULL', 'sqlsrv' => 'nvarchar(max) NOT NULL', @@ -812,7 +813,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_TEXT . ' CHECK (value LIKE \'test%\')', $this->text()->check('value LIKE \'test%\''), [ - 'postgres' => 'text CHECK (value LIKE \'test%\')', + 'pgsql' => 'text CHECK (value LIKE \'test%\')', 'oci' => 'CLOB CHECK (value LIKE \'test%\')', ], Schema::TYPE_TEXT . ' CHECK (value LIKE \'test%\')', @@ -822,7 +823,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->text()->notNull(), [ 'mysql' => 'text NOT NULL', - 'postgres' => 'text NOT NULL', + 'pgsql' => 'text NOT NULL', 'sqlite' => 'text NOT NULL', 'oci' => 'CLOB NOT NULL', 'sqlsrv' => 'nvarchar(max) NOT NULL', @@ -835,7 +836,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->text(), [ 'mysql' => 'text', - 'postgres' => 'text', + 'pgsql' => 'text', 'sqlite' => 'text', 'oci' => 'CLOB', 'sqlsrv' => 'nvarchar(max)', @@ -848,7 +849,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->text(), [ 'mysql' => 'text', - 'postgres' => 'text', + 'pgsql' => 'text', 'sqlite' => 'text', 'oci' => 'CLOB', 'sqlsrv' => 'nvarchar(max)', @@ -860,7 +861,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase // $this->time()->check("value BETWEEN '12:00:00' AND '13:01:01'"), // [ // 'mysql' => , - // 'postgres' => , + // 'pgsql' => , // 'sqlite' => , // 'sqlsrv' => , // 'cubrid' => , @@ -870,7 +871,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_TIME . ' NOT NULL', $this->time()->notNull(), [ - 'postgres' => 'time(0) NOT NULL', + 'pgsql' => 'time(0) NOT NULL', 'sqlite' => 'time NOT NULL', 'oci' => 'TIMESTAMP NOT NULL', 'sqlsrv' => 'time NOT NULL', @@ -881,7 +882,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_TIME, $this->time(), [ - 'postgres' => 'time(0)', + 'pgsql' => 'time(0)', 'sqlite' => 'time', 'oci' => 'TIMESTAMP', 'sqlsrv' => 'time', @@ -893,7 +894,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase // $this->timestamp()->check("value BETWEEN '2011-01-01' AND '2013-01-01'"), // [ // 'mysql' => , - // 'postgres' => , + // 'pgsql' => , // 'sqlite' => , // 'sqlsrv' => , // 'cubrid' => , @@ -903,7 +904,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_TIMESTAMP . ' NOT NULL', $this->timestamp()->notNull(), [ - 'postgres' => 'timestamp(0) NOT NULL', + 'pgsql' => 'timestamp(0) NOT NULL', 'sqlite' => 'timestamp NOT NULL', 'oci' => 'TIMESTAMP NOT NULL', 'sqlsrv' => 'datetime NOT NULL', @@ -919,7 +920,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase * @see \yiiunit\framework\db\mysql\QueryBuilderTest::columnTypes() */ - 'postgres' => 'timestamp(0)', + 'pgsql' => 'timestamp(0)', 'sqlite' => 'timestamp', 'oci' => 'TIMESTAMP', 'sqlsrv' => 'datetime', @@ -930,7 +931,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase Schema::TYPE_TIMESTAMP . ' NULL DEFAULT NULL', $this->timestamp()->defaultValue(null), [ - 'postgres' => 'timestamp(0) NULL DEFAULT NULL', + 'pgsql' => 'timestamp(0) NULL DEFAULT NULL', 'sqlite' => 'timestamp NULL DEFAULT NULL', 'sqlsrv' => 'datetime NULL DEFAULT NULL', 'cubrid' => 'timestamp NULL DEFAULT NULL', @@ -941,8 +942,8 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->primaryKey()->unsigned(), [ 'mysql' => 'int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY', - 'postgres' => 'serial NOT NULL PRIMARY KEY', - 'sqlite' => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL', + 'pgsql' => 'serial NOT NULL PRIMARY KEY', + 'sqlite' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', ], ], [ @@ -950,8 +951,8 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->bigPrimaryKey()->unsigned(), [ 'mysql' => 'bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY', - 'postgres' => 'bigserial NOT NULL PRIMARY KEY', - 'sqlite' => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL', + 'pgsql' => 'bigserial NOT NULL PRIMARY KEY', + 'sqlite' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', ], ], [ @@ -959,7 +960,6 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->integer()->comment('test comment'), [ 'mysql' => "int(11) COMMENT 'test comment'", - 'postgres' => 'integer', 'sqlsrv' => 'int', 'cubrid' => "int COMMENT 'test comment'", ], @@ -972,7 +972,6 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->primaryKey()->comment('test comment'), [ 'mysql' => "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'test comment'", - 'postgres' => 'serial NOT NULL PRIMARY KEY', 'sqlsrv' => 'int IDENTITY PRIMARY KEY', 'cubrid' => "int NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'test comment'", ], @@ -985,12 +984,11 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->primaryKey()->first(), [ 'mysql' => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST', - 'postgres' => 'serial NOT NULL PRIMARY KEY', - 'oci' => 'NUMBER(10) NOT NULL PRIMARY KEY', 'sqlsrv' => 'int IDENTITY PRIMARY KEY', 'cubrid' => 'int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST', ], [ + 'oci' => 'NUMBER(10) NOT NULL PRIMARY KEY', 'sqlsrv' => 'pk', ] ], @@ -999,12 +997,12 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->integer()->first(), [ 'mysql' => 'int(11) FIRST', - 'postgres' => 'integer', - 'oci' => 'NUMBER(10)', 'sqlsrv' => 'int', 'cubrid' => 'int FIRST', ], [ + 'oci' => 'NUMBER(10)', + 'pgsql' => 'integer', 'sqlsrv' => 'integer', ] ], @@ -1013,12 +1011,11 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->string()->first(), [ 'mysql' => 'varchar(255) FIRST', - 'postgres' => 'varchar(255)', - 'oci' => 'VARCHAR2(255)', 'sqlsrv' => 'nvarchar(255)', 'cubrid' => 'varchar(255) FIRST', ], [ + 'oci' => 'VARCHAR2(255)', 'sqlsrv' => 'string', ] ], @@ -1027,12 +1024,11 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->integer()->append('NOT NULL')->first(), [ 'mysql' => 'int(11) NOT NULL FIRST', - 'postgres' => 'integer NOT NULL', - 'oci' => 'NUMBER(10) NOT NULL', 'sqlsrv' => 'int NOT NULL', 'cubrid' => 'int NOT NULL FIRST', ], [ + 'oci' => 'NUMBER(10) NOT NULL', 'sqlsrv' => 'integer NOT NULL', ] ], @@ -1041,12 +1037,11 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->string()->append('NOT NULL')->first(), [ 'mysql' => 'varchar(255) NOT NULL FIRST', - 'postgres' => 'varchar(255) NOT NULL', - 'oci' => 'VARCHAR2(255) NOT NULL', 'sqlsrv' => 'nvarchar(255) NOT NULL', 'cubrid' => 'varchar(255) NOT NULL FIRST', ], [ + 'oci' => 'VARCHAR2(255) NOT NULL', 'sqlsrv' => 'string NOT NULL', ] ], @@ -1178,6 +1173,18 @@ abstract class QueryBuilderTest extends DatabaseTestCase [['in', 'id', new TraversableObject([1, 2, 3])], '[[id]] IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3]], + //in using array objects containing null value + [['in', 'id', new TraversableObject([1, null])], '[[id]]=:qp0 OR [[id]] IS NULL', [':qp0' => 1]], + [['in', 'id', new TraversableObject([1, 2, null])], '[[id]] IN (:qp0, :qp1) OR [[id]] IS NULL', [':qp0' => 1, ':qp1' => 2]], + + //not in using array object containing null value + [['not in', 'id', new TraversableObject([1, null])], '[[id]]<>:qp0 AND [[id]] IS NOT NULL', [':qp0' => 1]], + [['not in', 'id', new TraversableObject([1, 2, null])], '[[id]] NOT IN (:qp0, :qp1) AND [[id]] IS NOT NULL', [':qp0' => 1, ':qp1' => 2]], + + //in using array object containing only null value + [['in', 'id', new TraversableObject([null])], '[[id]] IS NULL', []], + [['not in', 'id', new TraversableObject([null])], '[[id]] IS NOT NULL', []], + 'composite in using array objects' => [ ['in', new TraversableObject(['id', 'name']), new TraversableObject([ ['id' => 1, 'name' => 'oy'], @@ -1186,6 +1193,15 @@ abstract class QueryBuilderTest extends DatabaseTestCase '([[id]], [[name]]) IN ((:qp0, :qp1), (:qp2, :qp3))', [':qp0' => 1, ':qp1' => 'oy', ':qp2' => 2, ':qp3' => 'yo'], ], + + // in object conditions + [new InCondition('id', 'in', 1), '[[id]]=:qp0', [':qp0' => 1]], + [new InCondition('id', 'in', [1]), '[[id]]=:qp0', [':qp0' => 1]], + [new InCondition('id', 'not in', 1), '[[id]]<>:qp0', [':qp0' => 1]], + [new InCondition('id', 'not in', [1]), '[[id]]<>:qp0', [':qp0' => 1]], + [new InCondition('id', 'in', [1, 2]), '[[id]] IN (:qp0, :qp1)', [':qp0' => 1, ':qp1' => 2]], + [new InCondition('id', 'not in', [1, 2]), '[[id]] NOT IN (:qp0, :qp1)', [':qp0' => 1, ':qp1' => 2]], + // exists [['exists', (new Query())->select('id')->from('users')->where(['active' => 1])], 'EXISTS (SELECT [[id]] FROM [[users]] WHERE [[active]]=:qp0)', [':qp0' => 1]], [['not exists', (new Query())->select('id')->from('users')->where(['active' => 1])], 'NOT EXISTS (SELECT [[id]] FROM [[users]] WHERE [[active]]=:qp0)', [':qp0' => 1]], @@ -1667,6 +1683,56 @@ abstract class QueryBuilderTest extends DatabaseTestCase $this->assertEquals([], $queryParams); } + public function testBuildWithQuery() + { + $expectedQuerySql = $this->replaceQuotes( + 'WITH a1 AS (SELECT [[id]] FROM [[t1]] WHERE expr = 1), a2 AS ((SELECT [[id]] FROM [[t2]] INNER JOIN [[a1]] ON t2.id = a1.id WHERE expr = 2) UNION ( SELECT [[id]] FROM [[t3]] WHERE expr = 3 )) SELECT * FROM [[a2]]' + ); + $with1Query = (new Query()) + ->select('id') + ->from('t1') + ->where('expr = 1'); + + $with2Query = (new Query()) + ->select('id') + ->from('t2') + ->innerJoin('a1', 't2.id = a1.id') + ->where('expr = 2'); + + $with3Query = (new Query()) + ->select('id') + ->from('t3') + ->where('expr = 3'); + + $query = (new Query()) + ->withQuery($with1Query, 'a1') + ->withQuery($with2Query->union($with3Query), 'a2') + ->from('a2'); + + list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals([], $queryParams); + } + + public function testBuildWithQueryRecursive() + { + $expectedQuerySql = $this->replaceQuotes( + 'WITH RECURSIVE a1 AS (SELECT [[id]] FROM [[t1]] WHERE expr = 1) SELECT * FROM [[a1]]' + ); + $with1Query = (new Query()) + ->select('id') + ->from('t1') + ->where('expr = 1'); + + $query = (new Query()) + ->withQuery($with1Query, 'a1', true) + ->from('a1'); + + list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals([], $queryParams); + } + public function testSelectSubquery() { $subquery = (new Query()) @@ -2407,6 +2473,19 @@ abstract class QueryBuilderTest extends DatabaseTestCase '[[location]].[[title_ru]] LIKE :qp0', [':qp0' => 'vi%'], ], + + // like object conditions + [new LikeCondition('name', 'like', new Expression('CONCAT("test", name, "%")')), '[[name]] LIKE CONCAT("test", name, "%")', []], + [new LikeCondition('name', 'not like', new Expression('CONCAT("test", name, "%")')), '[[name]] NOT LIKE CONCAT("test", name, "%")', []], + [new LikeCondition('name', 'or like', new Expression('CONCAT("test", name, "%")')), '[[name]] LIKE CONCAT("test", name, "%")', []], + [new LikeCondition('name', 'or not like', new Expression('CONCAT("test", name, "%")')), '[[name]] NOT LIKE CONCAT("test", name, "%")', []], + [new LikeCondition('name', 'like', [new Expression('CONCAT("test", name, "%")'), '\ab_c']), '[[name]] LIKE CONCAT("test", name, "%") AND [[name]] LIKE :qp0', [':qp0' => '%\\\ab\_c%']], + [new LikeCondition('name', 'not like', [new Expression('CONCAT("test", name, "%")'), '\ab_c']), '[[name]] NOT LIKE CONCAT("test", name, "%") AND [[name]] NOT LIKE :qp0', [':qp0' => '%\\\ab\_c%']], + [new LikeCondition('name', 'or like', [new Expression('CONCAT("test", name, "%")'), '\ab_c']), '[[name]] LIKE CONCAT("test", name, "%") OR [[name]] LIKE :qp0', [':qp0' => '%\\\ab\_c%']], + [new LikeCondition('name', 'or not like', [new Expression('CONCAT("test", name, "%")'), '\ab_c']), '[[name]] NOT LIKE CONCAT("test", name, "%") OR [[name]] NOT LIKE :qp0', [':qp0' => '%\\\ab\_c%']], + + // like with expression as columnName + [['like', new Expression('name'), 'teststring'], 'name LIKE :qp0', [':qp0' => "%teststring%"]], ]; // adjust dbms specific escaping diff --git a/tests/framework/db/QueryTest.php b/tests/framework/db/QueryTest.php index 1735b95..81eb2af 100644 --- a/tests/framework/db/QueryTest.php +++ b/tests/framework/db/QueryTest.php @@ -359,10 +359,10 @@ abstract class QueryTest extends DatabaseTestCase { $db = $this->getConnection(); - $result = (new Query())->from('customer')->where(['status' => 2])->one($db); + $result = (new Query())->from('customer')->where(['[[status]]' => 2])->one($db); $this->assertEquals('user3', $result['name']); - $result = (new Query())->from('customer')->where(['status' => 3])->one($db); + $result = (new Query())->from('customer')->where(['[[status]]' => 3])->one($db); $this->assertFalse($result); } @@ -391,6 +391,14 @@ abstract class QueryTest extends DatabaseTestCase ->column($db); $this->assertEquals([3 => 'user3', 2 => 'user2', 1 => 'user1'], $result); + // https://github.com/yiisoft/yii2/issues/17687 + $result = (new Query())->from('customer') + ->select('name') + ->orderBy(['id' => SORT_DESC]) + ->indexBy('customer.id') + ->column($db); + $this->assertEquals([3 => 'user3', 2 => 'user2', 1 => 'user1'], $result); + // https://github.com/yiisoft/yii2/issues/12649 $result = (new Query())->from('customer') ->select(['name', 'id']) @@ -657,7 +665,11 @@ abstract class QueryTest extends DatabaseTestCase { $db = $this->getConnection(); $query = (new Query()) - ->from(new \yii\db\Expression('(SELECT id, name, email, address, status FROM customer) c')) + ->from( + new \yii\db\Expression( + '(SELECT [[id]], [[name]], [[email]], [[address]], [[status]] FROM {{customer}}) c' + ) + ) ->where(['status' => 2]); $result = $query->one($db); @@ -716,4 +728,90 @@ abstract class QueryTest extends DatabaseTestCase $this->assertEquals('user11', $query->cache()->where(['id' => 1])->scalar($db)); }, 10); } + + + /** + * checks that all needed properties copied from source to new query + */ + public function testQueryCreation() + { + $where = 'id > :min_user_id'; + $limit = 50; + $offset = 2; + $orderBy = ['name' => SORT_ASC]; + $indexBy = 'id'; + $select = ['id' => 'id', 'name' => 'name', 'articles_count' => 'count(*)']; + $selectOption = 'SQL_NO_CACHE'; + $from = 'recent_users'; + $groupBy = 'id'; + $having = ['>', 'articles_count', 0]; + $params = [':min_user_id' => 100]; + list($joinType, $joinTable, $joinOn) = $join = ['INNER', 'articles', 'articles.author_id=users.id']; + + $unionQuery = (new Query()) + ->select('id, name, 1000 as articles_count') + ->from('admins'); + + $withQuery = (new Query()) + ->select('id, name') + ->from('users') + ->where('DATE(registered_at) > "2020-01-01"'); + + // build target query + $sourceQuery = (new Query()) + ->where($where) + ->limit($limit) + ->offset($offset) + ->orderBy($orderBy) + ->indexBy($indexBy) + ->select($select, $selectOption) + ->distinct() + ->from($from) + ->groupBy($groupBy) + ->having($having) + ->addParams($params) + ->join($joinType, $joinTable, $joinOn) + ->union($unionQuery) + ->withQuery($withQuery, $from); + + $newQuery = Query::create($sourceQuery); + + $this->assertEquals($where, $newQuery->where); + $this->assertEquals($limit, $newQuery->limit); + $this->assertEquals($offset, $newQuery->offset); + $this->assertEquals($orderBy, $newQuery->orderBy); + $this->assertEquals($indexBy, $newQuery->indexBy); + $this->assertEquals($select, $newQuery->select); + $this->assertEquals($selectOption, $newQuery->selectOption); + $this->assertTrue($newQuery->distinct); + $this->assertEquals([$from], $newQuery->from); + $this->assertEquals([$groupBy], $newQuery->groupBy); + $this->assertEquals($having, $newQuery->having); + $this->assertEquals($params, $newQuery->params); + $this->assertEquals([$join], $newQuery->join); + $this->assertEquals([['query' => $unionQuery, 'all' => false]], $newQuery->union); + $this->assertEquals( + [['query' => $withQuery, 'alias' => $from, 'recursive' => false]], + $newQuery->withQueries + ); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18499 + */ + public function testAllWithAutomaticallyAddedIndexedByColumn() + { + $db = $this->getConnection(); + + $result = (new Query())->from('customer') + ->select('name') + ->orderBy(['id' => SORT_DESC]) + ->indexBy('id') + ->all($db); + $this->assertEquals([ + 3 => ['name' => 'user3', 'id' => 3], + 2 => ['name' => 'user2', 'id' => 2], + 1 => ['name' => 'user1', 'id' => 1] + ], $result); + } } diff --git a/tests/framework/db/mssql/ActiveRecordTest.php b/tests/framework/db/mssql/ActiveRecordTest.php index 79c6f0e..7ac1686 100644 --- a/tests/framework/db/mssql/ActiveRecordTest.php +++ b/tests/framework/db/mssql/ActiveRecordTest.php @@ -7,6 +7,9 @@ namespace yiiunit\framework\db\mssql; +use yiiunit\data\ar\TestTrigger; +use yiiunit\data\ar\TestTriggerAlert; + /** * @group db * @group mssql @@ -19,4 +22,65 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest { $this->markTestSkipped('MSSQL does not support explicit value for an IDENTITY column.'); } + + /** + * @throws \yii\db\Exception + */ + public function testSaveWithTrigger() + { + $db = $this->getConnection(); + + // drop trigger if exist + $sql = 'IF (OBJECT_ID(N\'[dbo].[test_alert]\') IS NOT NULL) +BEGIN + DROP TRIGGER [dbo].[test_alert]; +END'; + $db->createCommand($sql)->execute(); + + // create trigger + $sql = 'CREATE TRIGGER [dbo].[test_alert] ON [dbo].[test_trigger] +AFTER INSERT +AS +BEGIN + INSERT INTO [dbo].[test_trigger_alert] ( [stringcol] ) + SELECT [stringcol] + FROM [inserted] +END'; + $db->createCommand($sql)->execute(); + + $record = new TestTrigger(); + $record->stringcol = 'test'; + $this->assertTrue($record->save(false)); + $this->assertEquals(1, $record->id); + + $testRecord = TestTriggerAlert::findOne(1); + $this->assertEquals('test', $testRecord->stringcol); + } + + /** + * @throws \yii\db\Exception + */ + public function testSaveWithComputedColumn() + { + $db = $this->getConnection(); + + $sql = 'IF OBJECT_ID(\'TESTFUNC\') IS NOT NULL EXEC(\'DROP FUNCTION TESTFUNC\')'; + $db->createCommand($sql)->execute(); + + $sql = 'CREATE FUNCTION TESTFUNC(@Number INT) +RETURNS VARCHAR(15) +AS +BEGIN + RETURN (SELECT CONVERT(VARCHAR(15),@Number)) +END'; + $db->createCommand($sql)->execute(); + + $sql = 'ALTER TABLE [dbo].[test_trigger] ADD [computed_column] AS dbo.TESTFUNC([ID])'; + $db->createCommand($sql)->execute(); + + $record = new TestTrigger(); + $record->stringcol = 'test'; + $this->assertTrue($record->save(false)); + $this->assertEquals(1, $record->id); + } } diff --git a/tests/framework/db/mssql/QueryBuilderTest.php b/tests/framework/db/mssql/QueryBuilderTest.php index 464af40..624ca72 100644 --- a/tests/framework/db/mssql/QueryBuilderTest.php +++ b/tests/framework/db/mssql/QueryBuilderTest.php @@ -7,6 +7,7 @@ namespace yiiunit\framework\db\mssql; +use yii\db\Expression; use yii\db\Query; use yiiunit\data\base\TraversableObject; @@ -262,6 +263,103 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest return $data; } + public function insertProvider() + { + return [ + 'regular-values' => [ + 'customer', + [ + 'email' => 'test@example.com', + 'name' => 'silverfire', + 'address' => 'Kyiv {{city}}, Ukraine', + 'is_active' => false, + 'related_id' => null, + ], + [], + $this->replaceQuotes('SET NOCOUNT ON;DECLARE @temporary_inserted TABLE ([id] int , [email] varchar(128) , [name] varchar(128) NULL, [address] text NULL, [status] int NULL, [profile_id] int NULL);' . + 'INSERT INTO [customer] ([email], [name], [address], [is_active], [related_id]) OUTPUT INSERTED.[id],INSERTED.[email],INSERTED.[name],INSERTED.[address],INSERTED.[status],INSERTED.[profile_id] INTO @temporary_inserted VALUES (:qp0, :qp1, :qp2, :qp3, :qp4);' . + 'SELECT * FROM @temporary_inserted'), + [ + ':qp0' => 'test@example.com', + ':qp1' => 'silverfire', + ':qp2' => 'Kyiv {{city}}, Ukraine', + ':qp3' => false, + ':qp4' => null, + ], + ], + 'params-and-expressions' => [ + '{{%type}}', + [ + '{{%type}}.[[related_id]]' => null, + '[[time]]' => new Expression('now()'), + ], + [], + 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE ([int_col] int , [int_col2] int NULL, [tinyint_col] tinyint NULL, [smallint_col] smallint NULL, [char_col] char(100) , [char_col2] varchar(100) NULL, [char_col3] text NULL, [float_col] decimal , [float_col2] float NULL, [blob_col] varbinary(MAX) NULL, [numeric_col] decimal NULL, [time] datetime , [bool_col] tinyint , [bool_col2] tinyint NULL);' . + 'INSERT INTO {{%type}} ({{%type}}.[[related_id]], [[time]]) OUTPUT INSERTED.[int_col],INSERTED.[int_col2],INSERTED.[tinyint_col],INSERTED.[smallint_col],INSERTED.[char_col],INSERTED.[char_col2],INSERTED.[char_col3],INSERTED.[float_col],INSERTED.[float_col2],INSERTED.[blob_col],INSERTED.[numeric_col],INSERTED.[time],INSERTED.[bool_col],INSERTED.[bool_col2] INTO @temporary_inserted VALUES (:qp0, now());' . + 'SELECT * FROM @temporary_inserted', + [ + ':qp0' => null, + ], + ], + 'carry passed params' => [ + 'customer', + [ + 'email' => 'test@example.com', + 'name' => 'sergeymakinen', + 'address' => '{{city}}', + 'is_active' => false, + 'related_id' => null, + 'col' => new Expression('CONCAT(:phFoo, :phBar)', [':phFoo' => 'foo']), + ], + [':phBar' => 'bar'], + $this->replaceQuotes('SET NOCOUNT ON;DECLARE @temporary_inserted TABLE ([id] int , [email] varchar(128) , [name] varchar(128) NULL, [address] text NULL, [status] int NULL, [profile_id] int NULL);' . + 'INSERT INTO [customer] ([email], [name], [address], [is_active], [related_id], [col]) OUTPUT INSERTED.[id],INSERTED.[email],INSERTED.[name],INSERTED.[address],INSERTED.[status],INSERTED.[profile_id] INTO @temporary_inserted VALUES (:qp1, :qp2, :qp3, :qp4, :qp5, CONCAT(:phFoo, :phBar));' . + 'SELECT * FROM @temporary_inserted'), + [ + ':phBar' => 'bar', + ':qp1' => 'test@example.com', + ':qp2' => 'sergeymakinen', + ':qp3' => '{{city}}', + ':qp4' => false, + ':qp5' => null, + ':phFoo' => 'foo', + ], + ], + 'carry passed params (query)' => [ + 'customer', + (new Query()) + ->select([ + 'email', + 'name', + 'address', + 'is_active', + 'related_id', + ]) + ->from('customer') + ->where([ + 'email' => 'test@example.com', + 'name' => 'sergeymakinen', + 'address' => '{{city}}', + 'is_active' => false, + 'related_id' => null, + 'col' => new Expression('CONCAT(:phFoo, :phBar)', [':phFoo' => 'foo']), + ]), + [':phBar' => 'bar'], + $this->replaceQuotes('SET NOCOUNT ON;DECLARE @temporary_inserted TABLE ([id] int , [email] varchar(128) , [name] varchar(128) NULL, [address] text NULL, [status] int NULL, [profile_id] int NULL);' . + 'INSERT INTO [customer] ([email], [name], [address], [is_active], [related_id]) OUTPUT INSERTED.[id],INSERTED.[email],INSERTED.[name],INSERTED.[address],INSERTED.[status],INSERTED.[profile_id] INTO @temporary_inserted SELECT [email], [name], [address], [is_active], [related_id] FROM [customer] WHERE ([email]=:qp1) AND ([name]=:qp2) AND ([address]=:qp3) AND ([is_active]=:qp4) AND ([related_id] IS NULL) AND ([col]=CONCAT(:phFoo, :phBar));' . + 'SELECT * FROM @temporary_inserted'), + [ + ':phBar' => 'bar', + ':qp1' => 'test@example.com', + ':qp2' => 'sergeymakinen', + ':qp3' => '{{city}}', + ':qp4' => false, + ':phFoo' => 'foo', + ], + ], + ]; + } + public function testResetSequence() { $qb = $this->getQueryBuilder(); @@ -297,13 +395,19 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest 3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (SELECT [email], 2 AS [status] FROM [customer] WHERE [name]=:qp0 ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS [EXCLUDED] ([email], [status]) ON ([T_upsert].[email]=[EXCLUDED].[email]) WHEN NOT MATCHED THEN INSERT ([email], [status]) VALUES ([EXCLUDED].[email], [EXCLUDED].[status]);', ], 'values and expressions' => [ - 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', + 3 => 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE ([id] int , [ts] int NULL, [email] varchar(128) , [recovery_email] varchar(128) NULL, [address] text NULL, [status] tinyint , [orders] int , [profile_id] int NULL);' . + 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) OUTPUT INSERTED.[id],INSERTED.[ts],INSERTED.[email],INSERTED.[recovery_email],INSERTED.[address],INSERTED.[status],INSERTED.[orders],INSERTED.[profile_id] INTO @temporary_inserted VALUES (:qp0, now());' . + 'SELECT * FROM @temporary_inserted', ], 'values and expressions with update part' => [ - 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', + 3 => 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE ([id] int , [ts] int NULL, [email] varchar(128) , [recovery_email] varchar(128) NULL, [address] text NULL, [status] tinyint , [orders] int , [profile_id] int NULL);' . + 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) OUTPUT INSERTED.[id],INSERTED.[ts],INSERTED.[email],INSERTED.[recovery_email],INSERTED.[address],INSERTED.[status],INSERTED.[orders],INSERTED.[profile_id] INTO @temporary_inserted VALUES (:qp0, now());' . + 'SELECT * FROM @temporary_inserted', ], 'values and expressions without update part' => [ - 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', + 3 => 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE ([id] int , [ts] int NULL, [email] varchar(128) , [recovery_email] varchar(128) NULL, [address] text NULL, [status] tinyint , [orders] int , [profile_id] int NULL);' . + 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) OUTPUT INSERTED.[id],INSERTED.[ts],INSERTED.[email],INSERTED.[recovery_email],INSERTED.[address],INSERTED.[status],INSERTED.[orders],INSERTED.[profile_id] INTO @temporary_inserted VALUES (:qp0, now());' . + 'SELECT * FROM @temporary_inserted', ], 'query, values and expressions with update part' => [ 3 => 'MERGE {{%T_upsert}} WITH (HOLDLOCK) USING (SELECT :phEmail AS [email], now() AS [[time]]) AS [EXCLUDED] ([email], [[time]]) ON ({{%T_upsert}}.[email]=[EXCLUDED].[email]) WHEN MATCHED THEN UPDATE SET [ts]=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ([email], [[time]]) VALUES ([EXCLUDED].[email], [EXCLUDED].[[time]]);', @@ -342,6 +446,301 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest return $data; } + public function testAlterColumn() + { + $qb = $this->getQueryBuilder(); + + $expected = "ALTER TABLE [foo1] ALTER COLUMN [bar] varchar(255) +DECLARE @tableName VARCHAR(MAX) = '[foo1]' +DECLARE @columnName VARCHAR(MAX) = 'bar' + +WHILE 1=1 BEGIN + DECLARE @constraintName NVARCHAR(128) + SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id]) + FROM ( + SELECT sc.[constid] object_id + FROM [sys].[sysconstraints] sc + JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName + WHERE sc.[id] = OBJECT_ID(@tableName) + UNION + SELECT object_id(i.[name]) FROM [sys].[indexes] i + JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName + JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id] + WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName) + ) cons + JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id] + WHERE so.[type]='D') + IF @constraintName IS NULL BREAK + EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']') +END"; + $sql = $qb->alterColumn('foo1', 'bar', 'varchar(255)'); + $this->assertEquals($expected, $sql); + + $expected = "ALTER TABLE [foo1] ALTER COLUMN [bar] nvarchar(255) NOT NULL +DECLARE @tableName VARCHAR(MAX) = '[foo1]' +DECLARE @columnName VARCHAR(MAX) = 'bar' + +WHILE 1=1 BEGIN + DECLARE @constraintName NVARCHAR(128) + SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id]) + FROM ( + SELECT sc.[constid] object_id + FROM [sys].[sysconstraints] sc + JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName + WHERE sc.[id] = OBJECT_ID(@tableName) + UNION + SELECT object_id(i.[name]) FROM [sys].[indexes] i + JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName + JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id] + WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName) + ) cons + JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id] + WHERE so.[type]='D') + IF @constraintName IS NULL BREAK + EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']') +END"; + $sql = $qb->alterColumn('foo1', 'bar', $this->string(255)->notNull()); + $this->assertEquals($expected, $sql); + + $expected = "ALTER TABLE [foo1] ALTER COLUMN [bar] nvarchar(255) +DECLARE @tableName VARCHAR(MAX) = '[foo1]' +DECLARE @columnName VARCHAR(MAX) = 'bar' + +WHILE 1=1 BEGIN + DECLARE @constraintName NVARCHAR(128) + SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id]) + FROM ( + SELECT sc.[constid] object_id + FROM [sys].[sysconstraints] sc + JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName + WHERE sc.[id] = OBJECT_ID(@tableName) + UNION + SELECT object_id(i.[name]) FROM [sys].[indexes] i + JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName + JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id] + WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName) + ) cons + JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id] + WHERE so.[type]='D') + IF @constraintName IS NULL BREAK + EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']') +END +ALTER TABLE [foo1] ADD CONSTRAINT [CK_foo1_bar] CHECK (LEN(bar) > 5)"; + $sql = $qb->alterColumn('foo1', 'bar', $this->string(255)->check('LEN(bar) > 5')); + $this->assertEquals($expected, $sql); + + $expected = "ALTER TABLE [foo1] ALTER COLUMN [bar] nvarchar(255) +DECLARE @tableName VARCHAR(MAX) = '[foo1]' +DECLARE @columnName VARCHAR(MAX) = 'bar' + +WHILE 1=1 BEGIN + DECLARE @constraintName NVARCHAR(128) + SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id]) + FROM ( + SELECT sc.[constid] object_id + FROM [sys].[sysconstraints] sc + JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName + WHERE sc.[id] = OBJECT_ID(@tableName) + UNION + SELECT object_id(i.[name]) FROM [sys].[indexes] i + JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName + JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id] + WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName) + ) cons + JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id] + WHERE so.[type]='D') + IF @constraintName IS NULL BREAK + EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']') +END +ALTER TABLE [foo1] ADD CONSTRAINT [DF_foo1_bar] DEFAULT '''''' FOR [bar]"; + $sql = $qb->alterColumn('foo1', 'bar', $this->string(255)->defaultValue('')); + $this->assertEquals($expected, $sql); + + $expected = "ALTER TABLE [foo1] ALTER COLUMN [bar] nvarchar(255) +DECLARE @tableName VARCHAR(MAX) = '[foo1]' +DECLARE @columnName VARCHAR(MAX) = 'bar' + +WHILE 1=1 BEGIN + DECLARE @constraintName NVARCHAR(128) + SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id]) + FROM ( + SELECT sc.[constid] object_id + FROM [sys].[sysconstraints] sc + JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName + WHERE sc.[id] = OBJECT_ID(@tableName) + UNION + SELECT object_id(i.[name]) FROM [sys].[indexes] i + JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName + JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id] + WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName) + ) cons + JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id] + WHERE so.[type]='D') + IF @constraintName IS NULL BREAK + EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']') +END +ALTER TABLE [foo1] ADD CONSTRAINT [DF_foo1_bar] DEFAULT '''AbCdE''' FOR [bar]"; + $sql = $qb->alterColumn('foo1', 'bar', $this->string(255)->defaultValue('AbCdE')); + $this->assertEquals($expected, $sql); + + $expected = "ALTER TABLE [foo1] ALTER COLUMN [bar] datetime +DECLARE @tableName VARCHAR(MAX) = '[foo1]' +DECLARE @columnName VARCHAR(MAX) = 'bar' + +WHILE 1=1 BEGIN + DECLARE @constraintName NVARCHAR(128) + SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id]) + FROM ( + SELECT sc.[constid] object_id + FROM [sys].[sysconstraints] sc + JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName + WHERE sc.[id] = OBJECT_ID(@tableName) + UNION + SELECT object_id(i.[name]) FROM [sys].[indexes] i + JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName + JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id] + WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName) + ) cons + JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id] + WHERE so.[type]='D') + IF @constraintName IS NULL BREAK + EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']') +END +ALTER TABLE [foo1] ADD CONSTRAINT [DF_foo1_bar] DEFAULT 'CURRENT_TIMESTAMP' FOR [bar]"; + $sql = $qb->alterColumn('foo1', 'bar', $this->timestamp()->defaultExpression('CURRENT_TIMESTAMP')); + $this->assertEquals($expected, $sql); + + $expected = "ALTER TABLE [foo1] ALTER COLUMN [bar] nvarchar(30) +DECLARE @tableName VARCHAR(MAX) = '[foo1]' +DECLARE @columnName VARCHAR(MAX) = 'bar' + +WHILE 1=1 BEGIN + DECLARE @constraintName NVARCHAR(128) + SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id]) + FROM ( + SELECT sc.[constid] object_id + FROM [sys].[sysconstraints] sc + JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName + WHERE sc.[id] = OBJECT_ID(@tableName) + UNION + SELECT object_id(i.[name]) FROM [sys].[indexes] i + JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName + JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id] + WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName) + ) cons + JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id] + WHERE so.[type]='D') + IF @constraintName IS NULL BREAK + EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']') +END +ALTER TABLE [foo1] ADD CONSTRAINT [UQ_foo1_bar] UNIQUE ([bar])"; + $sql = $qb->alterColumn('foo1', 'bar', $this->string(30)->unique()); + $this->assertEquals($expected, $sql); + } + + public function testAlterColumnOnDb() + { + $connection = $this->getConnection(); + + $sql = $connection->getQueryBuilder()->alterColumn('foo1', 'bar', 'varchar(255)'); + $connection->createCommand($sql)->execute(); + $schema = $connection->getTableSchema('[foo1]', true); + + $this->assertEquals("varchar(255)", $schema->getColumn('bar')->dbType); + $this->assertEquals(true, $schema->getColumn('bar')->allowNull); + + $sql = $connection->getQueryBuilder()->alterColumn('foo1', 'bar', $this->string(128)->notNull()); + $connection->createCommand($sql)->execute(); + $schema = $connection->getTableSchema('[foo1]', true); + $this->assertEquals("nvarchar(128)", $schema->getColumn('bar')->dbType); + $this->assertEquals(false, $schema->getColumn('bar')->allowNull); + } + + public function testAlterColumnWithCheckConstraintOnDb() + { + $connection = $this->getConnection(); + + $sql = $connection->getQueryBuilder()->alterColumn('foo1', 'bar', $this->string(128)->null()->check('LEN(bar) > 5')); + $connection->createCommand($sql)->execute(); + $schema = $connection->getTableSchema('[foo1]', true); + $this->assertEquals("nvarchar(128)", $schema->getColumn('bar')->dbType); + $this->assertEquals(true, $schema->getColumn('bar')->allowNull); + + $sql = "INSERT INTO [foo1]([bar]) values('abcdef')"; + $this->assertEquals(1, $connection->createCommand($sql)->execute()); + } + + public function testAlterColumnWithCheckConstraintOnDbWithException() + { + $connection = $this->getConnection(); + + $sql = $connection->getQueryBuilder()->alterColumn('foo1', 'bar', $this->string(64)->check('LEN(bar) > 5')); + $connection->createCommand($sql)->execute(); + + $sql = "INSERT INTO [foo1]([bar]) values('abcde')"; + $this->expectException('yii\db\IntegrityException'); + $this->assertEquals(1, $connection->createCommand($sql)->execute()); + } + + public function testAlterColumnWithUniqueConstraintOnDbWithException() + { + $connection = $this->getConnection(); + + $sql = $connection->getQueryBuilder()->alterColumn('foo1', 'bar', $this->string(64)->unique()); + $connection->createCommand($sql)->execute(); + + $sql = "INSERT INTO [foo1]([bar]) values('abcdef')"; + $this->assertEquals(1, $connection->createCommand($sql)->execute()); + + $this->expectException('yii\db\IntegrityException'); + $this->assertEquals(1, $connection->createCommand($sql)->execute()); + } + + public function testDropColumn() + { + $qb = $this->getQueryBuilder(); + + $expected = "DECLARE @tableName VARCHAR(MAX) = '[foo1]' +DECLARE @columnName VARCHAR(MAX) = 'bar' + +WHILE 1=1 BEGIN + DECLARE @constraintName NVARCHAR(128) + SET @constraintName = (SELECT TOP 1 OBJECT_NAME(cons.[object_id]) + FROM ( + SELECT sc.[constid] object_id + FROM [sys].[sysconstraints] sc + JOIN [sys].[columns] c ON c.[object_id]=sc.[id] AND c.[column_id]=sc.[colid] AND c.[name]=@columnName + WHERE sc.[id] = OBJECT_ID(@tableName) + UNION + SELECT object_id(i.[name]) FROM [sys].[indexes] i + JOIN [sys].[columns] c ON c.[object_id]=i.[object_id] AND c.[name]=@columnName + JOIN [sys].[index_columns] ic ON ic.[object_id]=i.[object_id] AND i.[index_id]=ic.[index_id] AND c.[column_id]=ic.[column_id] + WHERE i.[is_unique_constraint]=1 and i.[object_id]=OBJECT_ID(@tableName) + ) cons + JOIN [sys].[objects] so ON so.[object_id]=cons.[object_id] + ) + IF @constraintName IS NULL BREAK + EXEC (N'ALTER TABLE ' + @tableName + ' DROP CONSTRAINT [' + @constraintName + ']') +END +ALTER TABLE [foo1] DROP COLUMN [bar]"; + $sql = $qb->dropColumn('foo1', 'bar'); + $this->assertEquals($expected, $sql); + } + + public function testDropColumnOnDb() + { + $connection = $this->getConnection(); + + $sql = $connection->getQueryBuilder()->alterColumn('foo1', 'bar', $this->string(64)->defaultValue("")->check('LEN(bar) < 5')->unique()); + $connection->createCommand($sql)->execute(); + + $sql = $connection->getQueryBuilder()->dropColumn('foo1', 'bar'); + $this->assertEquals(0, $connection->createCommand($sql)->execute()); + + $schema = $connection->getTableSchema('[foo1]', true); + $this->assertEquals(NULL, $schema->getColumn('bar')); + } + public function buildFromDataProvider() { $data = parent::buildFromDataProvider(); diff --git a/tests/framework/db/mssql/SchemaTest.php b/tests/framework/db/mssql/SchemaTest.php index a1d859a..28f456b 100644 --- a/tests/framework/db/mssql/SchemaTest.php +++ b/tests/framework/db/mssql/SchemaTest.php @@ -8,6 +8,7 @@ namespace yiiunit\framework\db\mssql; use yii\db\DefaultValueConstraint; +use yii\db\mssql\Schema; use yiiunit\framework\db\AnyValue; /** @@ -180,4 +181,23 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest return $columns; } + + public function testGetPrimaryKey() + { + $db = $this->getConnection(); + + if ($db->getSchema()->getTableSchema('testPKTable') !== null) { + $db->createCommand()->dropTable('testPKTable')->execute(); + } + + $db->createCommand()->createTable( + 'testPKTable', + ['id' => Schema::TYPE_PK, 'bar' => Schema::TYPE_INTEGER] + )->execute(); + + $insertResult = $db->getSchema()->insert('testPKTable', ['bar' => 1]); + $selectResult = $db->createCommand('select [id] from [testPKTable] where [bar]=1')->queryOne(); + + $this->assertEquals($selectResult['id'], $insertResult['id']); + } } diff --git a/tests/framework/db/mysql/ColumnSchemaBuilderTest.php b/tests/framework/db/mysql/ColumnSchemaBuilderTest.php index 7f1b0ff..83d5ccd 100644 --- a/tests/framework/db/mysql/ColumnSchemaBuilderTest.php +++ b/tests/framework/db/mysql/ColumnSchemaBuilderTest.php @@ -8,7 +8,7 @@ namespace yiiunit\framework\db\mysql; use yii\db\mysql\ColumnSchemaBuilder; -use yii\db\Schema; +use yii\db\mysql\Schema; /** * ColumnSchemaBuilderTest tests ColumnSchemaBuilder for MySQL. @@ -44,6 +44,12 @@ class ColumnSchemaBuilderTest extends \yiiunit\framework\db\ColumnSchemaBuilderT ['integer(10) COMMENT \'test\'', Schema::TYPE_INTEGER, 10, [ ['comment', 'test'], ]], + // https://github.com/yiisoft/yii2/issues/11945 # TODO: real test against database + ['string(50) NOT NULL COMMENT \'Property name\' COLLATE ascii_general_ci', Schema::TYPE_STRING, 50, [ + ['comment', 'Property name'], + ['append', 'COLLATE ascii_general_ci'], + ['notNull'] + ]], ]; } } diff --git a/tests/framework/db/mysql/ConnectionTest.php b/tests/framework/db/mysql/ConnectionTest.php index 7f9b156..8a8f36e 100644 --- a/tests/framework/db/mysql/ConnectionTest.php +++ b/tests/framework/db/mysql/ConnectionTest.php @@ -7,6 +7,8 @@ namespace yiiunit\framework\db\mysql; +use yii\db\Connection; + /** * @group db * @group mysql @@ -14,4 +16,21 @@ namespace yiiunit\framework\db\mysql; class ConnectionTest extends \yiiunit\framework\db\ConnectionTest { protected $driverName = 'mysql'; + + /** + * @doesNotPerformAssertions + */ + public function testTransactionAutocommit() + { + /** @var Connection $connection */ + $connection = $this->getConnection(true); + $connection->transaction(function (Connection $db) { + // create table will cause the transaction to be implicitly committed + // (see https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html) + $name = 'test_implicit_transaction_table'; + $db->createCommand()->createTable($name, ['id' => 'pk'])->execute(); + $db->createCommand()->dropTable($name)->execute(); + }); + // If we made it this far without an error, then everything's working + } } diff --git a/tests/framework/db/mysql/QueryBuilderTest.php b/tests/framework/db/mysql/QueryBuilderTest.php index ebb89d7..2865dc3 100644 --- a/tests/framework/db/mysql/QueryBuilderTest.php +++ b/tests/framework/db/mysql/QueryBuilderTest.php @@ -367,4 +367,30 @@ MySqlStatement; $this->assertNotFalse($commentPos); $this->assertLessThan($checkPos, $commentPos); } + + /** + * Test for issue https://github.com/yiisoft/yii2/issues/14663 + */ + public function testInsertInteger() + { + $db = $this->getConnection(); + + $command = $db->createCommand(); + + $sql = $command->insert( + '{{customer}}', + [ + 'profile_id' => 22, + ] + )->getRawSql(); + $this->assertEquals('INSERT INTO `customer` (`profile_id`) VALUES (22)', $sql); + + $sql = $command->insert( + '{{customer}}', + [ + 'profile_id' => '1000000000000', + ] + )->getRawSql(); + $this->assertEquals('INSERT INTO `customer` (`profile_id`) VALUES (1000000000000)', $sql); + } } diff --git a/tests/framework/db/mysql/SchemaTest.php b/tests/framework/db/mysql/SchemaTest.php index c182bb2..6fa6a5d 100644 --- a/tests/framework/db/mysql/SchemaTest.php +++ b/tests/framework/db/mysql/SchemaTest.php @@ -38,7 +38,33 @@ SQL; $dt = $schema->columns['dt']; - $this->assertInstanceOf(Expression::className(),$dt->defaultValue); + $this->assertInstanceOf(Expression::className(), $dt->defaultValue); + $this->assertEquals('CURRENT_TIMESTAMP', (string)$dt->defaultValue); + } + + public function testDefaultDatetimeColumnWithMicrosecs() + { + if (!version_compare($this->getConnection()->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '5.6.4', '>=')) { + $this->markTestSkipped('CURRENT_TIMESTAMP with microseconds as default column value is supported since MySQL 5.6.4.'); + } + $sql = <<getConnection()->createCommand($sql)->execute(); + + $schema = $this->getConnection()->getTableSchema('current_timestamp_test'); + + $dt = $schema->columns['dt']; + $this->assertInstanceOf(Expression::className(), $dt->defaultValue); + $this->assertEquals('CURRENT_TIMESTAMP(2)', (string)$dt->defaultValue); + + $ts = $schema->columns['ts']; + $this->assertInstanceOf(Expression::className(), $ts->defaultValue); + $this->assertEquals('CURRENT_TIMESTAMP(3)', (string)$ts->defaultValue); } public function testGetSchemaNames() @@ -92,4 +118,63 @@ SQL; $this->assertInstanceOf(Expression::className(), $column->defaultValue); $this->assertEquals('CURRENT_TIMESTAMP', $column->defaultValue); } + + public function getExpectedColumns() + { + $version = $this->getConnection()->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION); + + return array_merge( + parent::getExpectedColumns(), + [ + 'int_col' => [ + 'type' => 'integer', + 'dbType' => \version_compare($version, '8.0.17', '>') ? 'int' : 'int(11)', + 'phpType' => 'integer', + 'allowNull' => false, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'precision' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'scale' => null, + 'defaultValue' => null, + ], + 'int_col2' => [ + 'type' => 'integer', + 'dbType' => \version_compare($version, '8.0.17', '>') ? 'int' : 'int(11)', + 'phpType' => 'integer', + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'precision' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'scale' => null, + 'defaultValue' => 1, + ], + 'tinyint_col' => [ + 'type' => 'tinyint', + 'dbType' => \version_compare($version, '8.0.17', '>') ? 'tinyint' : 'tinyint(3)', + 'phpType' => 'integer', + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => \version_compare($version, '8.0.17', '>') ? null : 3, + 'precision' => \version_compare($version, '8.0.17', '>') ? null : 3, + 'scale' => null, + 'defaultValue' => 1, + ], + 'smallint_col' => [ + 'type' => 'smallint', + 'dbType' => \version_compare($version, '8.0.17', '>') ? 'smallint' : 'smallint(1)', + 'phpType' => 'integer', + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => \version_compare($version, '8.0.17', '>') ? null : 1, + 'precision' => \version_compare($version, '8.0.17', '>') ? null : 1, + 'scale' => null, + 'defaultValue' => 1, + ], + ] + ); + } } diff --git a/tests/framework/db/mysql/connection/DeadLockTest.php b/tests/framework/db/mysql/connection/DeadLockTest.php index 316b1fb..4e6dd84 100644 --- a/tests/framework/db/mysql/connection/DeadLockTest.php +++ b/tests/framework/db/mysql/connection/DeadLockTest.php @@ -31,10 +31,6 @@ class DeadLockTest extends \yiiunit\framework\db\mysql\ConnectionTest */ public function testDeadlockException() { - if (getenv('TRAVIS') && PHP_VERSION_ID < 70000) { - $this->markTestSkipped('Skipping PHP 5 on Travis since it segfaults with pcntl'); - } - if (!\function_exists('pcntl_fork')) { $this->markTestSkipped('pcntl_fork() is not available'); } diff --git a/tests/framework/db/oci/ActiveRecordTest.php b/tests/framework/db/oci/ActiveRecordTest.php index ad5bee0..91442a5 100644 --- a/tests/framework/db/oci/ActiveRecordTest.php +++ b/tests/framework/db/oci/ActiveRecordTest.php @@ -7,7 +7,12 @@ namespace yiiunit\framework\db\oci; +use yii\db\Expression; +use yiiunit\data\ar\BitValues; +use yiiunit\data\ar\Customer; use yiiunit\data\ar\DefaultPk; +use yiiunit\data\ar\DefaultMultiplePk; +use yiiunit\data\ar\Order; use yiiunit\data\ar\Type; /** @@ -43,10 +48,10 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest $this->assertSame('1337', trim($model->char_col)); $this->assertSame('test', $model->char_col2); $this->assertSame('test123', $model->char_col3); -// $this->assertSame(1337.42, $model->float_col); -// $this->assertSame(42.1337, $model->float_col2); -// $this->assertTrue($model->bool_col); -// $this->assertFalse($model->bool_col2); + $this->assertSame(1337.42, $model->float_col); + $this->assertSame(42.1337, $model->float_col2); + $this->assertEquals('1', $model->bool_col); + $this->assertEquals('0', $model->bool_col2); } public function testDefaultValues() @@ -57,7 +62,7 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest $this->assertEquals('something', $model->char_col2); $this->assertEquals(1.23, $model->float_col2); $this->assertEquals(33.22, $model->numeric_col); - $this->assertTrue($model->bool_col2); + $this->assertEquals('1', $model->bool_col2); // not testing $model->time, because oci\Schema can't read default value @@ -121,4 +126,200 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest $record->save(false); $this->assertEquals(5, $record->primaryKey); } + + public function testMultiplePrimaryKeyAfterSave() + { + $record = new DefaultMultiplePk(); + $record->id = 5; + $record->second_key_column = 'secondKey'; + $record->type = 'type'; + $record->save(false); + $this->assertEquals(5, $record->id); + $this->assertEquals('secondKey', $record->second_key_column); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/9006 + */ + public function testBit() + { + $falseBit = BitValues::findOne(1); + $this->assertEquals('0', $falseBit->val); + + $trueBit = BitValues::findOne(2); + $this->assertEquals('1', $trueBit->val); + } + + /** + * Some PDO implementations(e.g. cubrid) do not support boolean values. + * Make sure this does not affect AR layer. + */ + public function testBooleanAttribute() + { + /* @var $customerClass \yii\db\ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + $customer = new $customerClass(); + $customer->name = 'boolean customer'; + $customer->email = 'mail@example.com'; + $customer->status = '1'; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals('1', $customer->status); + + $customer->status = '0'; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals('0', $customer->status); + + $customers = $customerClass::find()->where(['[[status]]' => '1'])->all(); + $this->assertCount(2, $customers); + + $customers = $customerClass::find()->where(['[[status]]' => '0'])->all(); + $this->assertCount(1, $customers); + } + + /** + * Tests the alias syntax for joinWith: 'alias' => 'relation'. + * @dataProvider aliasMethodProvider + * @param string $aliasMethod whether alias is specified explicitly or using the query syntax {{@tablename}} + */ + public function testJoinWithAlias($aliasMethod) + { + // left join and eager loading + /** @var ActiveQuery $query */ + $query = Order::find()->joinWith(['customer c']); + if ($aliasMethod === 'explicit') { + $orders = $query->orderBy('c.id DESC, order.id')->all(); + } elseif ($aliasMethod === 'querysyntax') { + $orders = $query->orderBy('{{@customer}}.id DESC, {{@order}}.id')->all(); + } elseif ($aliasMethod === 'applyAlias') { + $orders = $query->orderBy($query->applyAlias('customer', 'id') . ' DESC,' . $query->applyAlias('order', 'id'))->all(); + } + $this->assertCount(3, $orders); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertEquals(1, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + $this->assertTrue($orders[2]->isRelationPopulated('customer')); + + // inner join filtering and eager loading + $query = Order::find()->innerJoinWith(['customer c']); + if ($aliasMethod === 'explicit') { + $orders = $query->where('{{c}}.[[id]]=2')->orderBy('order.id')->all(); + } elseif ($aliasMethod === 'querysyntax') { + $orders = $query->where('{{@customer}}.[[id]]=2')->orderBy('{{@order}}.id')->all(); + } elseif ($aliasMethod === 'applyAlias') { + $orders = $query->where([$query->applyAlias('customer', 'id') => 2])->orderBy($query->applyAlias('order', 'id'))->all(); + } + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + + // inner join filtering without eager loading + $query = Order::find()->innerJoinWith(['customer c'], false); + if ($aliasMethod === 'explicit') { + $orders = $query->where('{{c}}.[[id]]=2')->orderBy('order.id')->all(); + } elseif ($aliasMethod === 'querysyntax') { + $orders = $query->where('{{@customer}}.[[id]]=2')->orderBy('{{@order}}.id')->all(); + } elseif ($aliasMethod === 'applyAlias') { + $orders = $query->where([$query->applyAlias('customer', 'id') => 2])->orderBy($query->applyAlias('order', 'id'))->all(); + } + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertFalse($orders[0]->isRelationPopulated('customer')); + $this->assertFalse($orders[1]->isRelationPopulated('customer')); + + // join with via-relation + $query = Order::find()->innerJoinWith(['books b']); + if ($aliasMethod === 'explicit') { + $orders = $query->where(['b.name' => 'Yii 1.1 Application Development Cookbook'])->orderBy('order.id')->all(); + } elseif ($aliasMethod === 'querysyntax') { + $orders = $query->where(['{{@item}}.name' => 'Yii 1.1 Application Development Cookbook'])->orderBy('{{@order}}.id')->all(); + } elseif ($aliasMethod === 'applyAlias') { + $orders = $query->where([$query->applyAlias('book', 'name') => 'Yii 1.1 Application Development Cookbook'])->orderBy($query->applyAlias('order', 'id'))->all(); + } + $this->assertCount(2, $orders); + $this->assertEquals(1, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertTrue($orders[0]->isRelationPopulated('books')); + $this->assertTrue($orders[1]->isRelationPopulated('books')); + $this->assertCount(2, $orders[0]->books); + $this->assertCount(1, $orders[1]->books); + + // joining sub relations + $query = Order::find()->innerJoinWith([ + 'items i' => function ($q) use ($aliasMethod) { + /* @var $q ActiveQuery */ + if ($aliasMethod === 'explicit') { + $q->orderBy('{{i}}.id'); + } elseif ($aliasMethod === 'querysyntax') { + $q->orderBy('{{@item}}.id'); + } elseif ($aliasMethod === 'applyAlias') { + $q->orderBy($q->applyAlias('item', 'id')); + } + }, + 'items.category c' => function ($q) use ($aliasMethod) { + /* @var $q ActiveQuery */ + if ($aliasMethod === 'explicit') { + $q->where('{{c}}.[[id]] = 2'); + } elseif ($aliasMethod === 'querysyntax') { + $q->where('{{@category}}.[[id]] = 2'); + } elseif ($aliasMethod === 'applyAlias') { + $q->where([$q->applyAlias('category', 'id') => 2]); + } + }, + ]); + if ($aliasMethod === 'explicit') { + $orders = $query->orderBy('{{i}}.id')->all(); + } elseif ($aliasMethod === 'querysyntax') { + $orders = $query->orderBy('{{@item}}.id')->all(); + } elseif ($aliasMethod === 'applyAlias') { + $orders = $query->orderBy($query->applyAlias('item', 'id'))->all(); + } + $this->assertCount(1, $orders); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + $this->assertEquals(2, $orders[0]->id); + $this->assertCount(3, $orders[0]->items); + $this->assertTrue($orders[0]->items[0]->isRelationPopulated('category')); + $this->assertEquals(2, $orders[0]->items[0]->category->id); + + // join with ON condition + if ($aliasMethod === 'explicit' || $aliasMethod === 'querysyntax') { + $relationName = 'books' . ucfirst($aliasMethod); + $orders = Order::find()->joinWith(["$relationName b"])->orderBy('order.id')->all(); + $this->assertCount(3, $orders); + $this->assertEquals(1, $orders[0]->id); + $this->assertEquals(2, $orders[1]->id); + $this->assertEquals(3, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated($relationName)); + $this->assertTrue($orders[1]->isRelationPopulated($relationName)); + $this->assertTrue($orders[2]->isRelationPopulated($relationName)); + $this->assertCount(2, $orders[0]->$relationName); + $this->assertCount(0, $orders[1]->$relationName); + $this->assertCount(1, $orders[2]->$relationName); + } + + // join with ON condition and alias in relation definition + if ($aliasMethod === 'explicit' || $aliasMethod === 'querysyntax') { + $relationName = 'books' . ucfirst($aliasMethod) . 'A'; + $orders = Order::find()->joinWith([(string)$relationName])->orderBy('order.id')->all(); + $this->assertCount(3, $orders); + $this->assertEquals(1, $orders[0]->id); + $this->assertEquals(2, $orders[1]->id); + $this->assertEquals(3, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated($relationName)); + $this->assertTrue($orders[1]->isRelationPopulated($relationName)); + $this->assertTrue($orders[2]->isRelationPopulated($relationName)); + $this->assertCount(2, $orders[0]->$relationName); + $this->assertCount(0, $orders[1]->$relationName); + $this->assertCount(1, $orders[2]->$relationName); + } + } } diff --git a/tests/framework/db/oci/CommandTest.php b/tests/framework/db/oci/CommandTest.php index b26d2e4..319f3fc 100644 --- a/tests/framework/db/oci/CommandTest.php +++ b/tests/framework/db/oci/CommandTest.php @@ -7,6 +7,11 @@ namespace yiiunit\framework\db\oci; +use yii\caching\ArrayCache; +use yii\db\Connection; +use yii\db\Query; +use yii\db\Schema; + /** * @group db * @group oci @@ -37,9 +42,445 @@ class CommandTest extends \yiiunit\framework\db\CommandTest public function batchInsertSqlProvider() { $data = parent::batchInsertSqlProvider(); - $data['issue11242']['expected'] = 'INSERT INTO "type" ("int_col", "float_col", "char_col") VALUES (NULL, NULL, \'Kyiv {{city}}, Ukraine\')'; - $data['wrongBehavior']['expected'] = 'INSERT INTO "type" ("type"."int_col", "float_col", "char_col") VALUES (\'\', \'\', \'Kyiv {{city}}, Ukraine\')'; + $data['issue11242']['expected'] = 'INSERT ALL INTO "type" ("int_col", "float_col", "char_col") ' . + "VALUES (NULL, NULL, 'Kyiv {{city}}, Ukraine') SELECT 1 FROM SYS.DUAL"; + $data['wrongBehavior']['expected'] = 'INSERT ALL INTO "type" ("type"."int_col", "float_col", "char_col") ' . + "VALUES ('', '', 'Kyiv {{city}}, Ukraine') SELECT 1 FROM SYS.DUAL"; + $data['batchInsert binds params from expression']['expected'] = 'INSERT ALL INTO "type" ("int_col") ' . + 'VALUES (:qp1) SELECT 1 FROM SYS.DUAL'; return $data; } + + /** + * Testing the "ORA-01461: can bind a LONG value only for insert into a LONG column" + * + * @return void + */ + public function testCLOBStringInsertion() + { + $db = $this->getConnection(); + + if ($db->getSchema()->getTableSchema('longstring') !== null) { + $db->createCommand()->dropTable('longstring')->execute(); + } + + $db->createCommand()->createTable('longstring', ['message' => Schema::TYPE_TEXT])->execute(); + + $longData = str_pad('-', 4001, '-=', STR_PAD_LEFT); + $db->createCommand()->insert('longstring', [ + 'message' => $longData, + ])->execute(); + + $this->assertEquals(1, $db->createCommand('SELECT count(*) FROM {{longstring}}')->queryScalar()); + + $db->createCommand()->dropTable('longstring')->execute(); + } + + public function testQueryCache() + { + $db = $this->getConnection(true); + + $db->enableQueryCache = true; + $db->queryCache = new ArrayCache(); + + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); + + $this->assertEquals('user1', $command->bindValue(':id', 1)->queryScalar()); + + $update = $db->createCommand('UPDATE {{customer}} SET [[name]] = :name WHERE [[id]] = :id'); + $update->bindValues([':id' => 1, ':name' => 'user11'])->execute(); + + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); + + $this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar()); + + $db->cache(function (Connection $db) use ($update) { + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); + + $this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar()); + + $update->bindValues([':id' => 2, ':name' => 'user22'])->execute(); + + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); + + $this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar()); + + $db->noCache(function () use ($db) { + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); + + $this->assertEquals('user22', $command->bindValue(':id', 2)->queryScalar()); + }); + + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); + + $this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar()); + }, 10); + + $db->enableQueryCache =false; + + $db->cache(function (Connection $db) use ($update) { + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); + + $this->assertEquals('user22', $command->bindValue(':id', 2)->queryScalar()); + + $update->bindValues([':id' => 2, ':name' => 'user2'])->execute(); + + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); + + $this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar()); + }, 10); + + $db->enableQueryCache = true; + + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id')->cache(); + + $this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar()); + + $update->bindValues([':id' => 1, ':name' => 'user1'])->execute(); + + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id')->cache(); + + $this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar()); + + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id')->noCache(); + + $this->assertEquals('user1', $command->bindValue(':id', 1)->queryScalar()); + + $db->cache(function (Connection $db) use ($update) { + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id'); + + $this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar()); + + $command = $db->createCommand('SELECT [[name]] FROM {{customer}} WHERE [[id]] = :id')->noCache(); + + $this->assertEquals('user1', $command->bindValue(':id', 1)->queryScalar()); + }, 10); + } + + public function paramsNonWhereProvider() + { + return [ + ['SELECT SUBSTR([[name]], :len) FROM {{customer}} WHERE [[email]] = :email GROUP BY SUBSTR([[name]], :len)'], + ['SELECT SUBSTR([[name]], :len) FROM {{customer}} WHERE [[email]] = :email ORDER BY SUBSTR([[name]], :len)'], + ['SELECT SUBSTR([[name]], :len) FROM {{customer}} WHERE [[email]] = :email'], + ]; + } + + public function testInsert() + { + $db = $this->getConnection(); + $db->createCommand('DELETE FROM {{customer}}')->execute(); + + $command = $db->createCommand(); + $command->insert( + '{{customer}}', + [ + 'email' => 't1@example.com', + 'name' => 'test', + 'address' => 'test address', + ] + )->execute(); + $this->assertEquals(1, $db->createCommand('SELECT COUNT(*) FROM {{customer}}')->queryScalar()); + $record = $db->createCommand('SELECT [[email]], [[name]], [[address]] FROM {{customer}}')->queryOne(); + $this->assertEquals([ + 'email' => 't1@example.com', + 'name' => 'test', + 'address' => 'test address', + ], $record); + } + + /** + * Test INSERT INTO ... SELECT SQL statement with alias syntax. + */ + public function testInsertSelectAlias() + { + $db = $this->getConnection(); + + $db->createCommand('DELETE FROM {{customer}}')->execute(); + + $command = $db->createCommand(); + + $command->insert( + '{{customer}}', + [ + 'email' => 't1@example.com', + 'name' => 'test', + 'address' => 'test address', + ] + )->execute(); + + $query = $db->createCommand( + "SELECT 't2@example.com' as [[email]], [[address]] as [[name]], [[name]] as [[address]] from {{customer}}" + ); + + $command->insert( + '{{customer}}', + $query->queryOne() + )->execute(); + + $this->assertEquals(2, $db->createCommand('SELECT COUNT(*) FROM {{customer}}')->queryScalar()); + + $record = $db->createCommand('SELECT [[email]], [[name]], [[address]] FROM {{customer}}')->queryAll(); + + $this->assertEquals([ + [ + 'email' => 't1@example.com', + 'name' => 'test', + 'address' => 'test address', + ], + [ + 'email' => 't2@example.com', + 'name' => 'test address', + 'address' => 'test', + ], + ], $record); + } + + /** + * Test batch insert with different data types. + * + * Ensure double is inserted with `.` decimal separator. + * + * https://github.com/yiisoft/yii2/issues/6526 + */ + public function testBatchInsertDataTypesLocale() + { + $locale = setlocale(LC_NUMERIC, 0); + if (false === $locale) { + $this->markTestSkipped('Your platform does not support locales.'); + } + $db = $this->getConnection(); + + try { + // This one sets decimal mark to comma sign + setlocale(LC_NUMERIC, 'ru_RU.UTF-8'); + + $cols = ['int_col', 'char_col', 'float_col', 'bool_col']; + $data = [ + [1, 'A', 9.735, '1'], + [2, 'B', -2.123, '0'], + [3, 'C', 2.123, '0'], + ]; + + // clear data in "type" table + $db->createCommand()->delete('type')->execute(); + // batch insert on "type" table + $db->createCommand()->batchInsert('type', $cols, $data)->execute(); + + // change , for point oracle. + $db->createCommand("ALTER SESSION SET NLS_NUMERIC_CHARACTERS='.,'")->execute(); + + $data = $db->createCommand( + 'SELECT [[int_col]], [[char_col]], [[float_col]], [[bool_col]] FROM {{type}} WHERE [[int_col]] ' . + 'IN (1,2,3) ORDER BY [[int_col]]' + )->queryAll(); + + $this->assertEquals(3, \count($data)); + $this->assertEquals(1, $data[0]['int_col']); + $this->assertEquals(2, $data[1]['int_col']); + $this->assertEquals(3, $data[2]['int_col']); + $this->assertEquals('A', rtrim($data[0]['char_col'])); // rtrim because Postgres padds the column with whitespace + $this->assertEquals('B', rtrim($data[1]['char_col'])); + $this->assertEquals('C', rtrim($data[2]['char_col'])); + $this->assertEquals('9.735', $data[0]['float_col']); + $this->assertEquals('-2.123', $data[1]['float_col']); + $this->assertEquals('2.123', $data[2]['float_col']); + $this->assertEquals('1', $data[0]['bool_col']); + $this->assertIsOneOf($data[1]['bool_col'], ['0', false]); + $this->assertIsOneOf($data[2]['bool_col'], ['0', false]); + + } catch (\Exception $e) { + setlocale(LC_NUMERIC, $locale); + throw $e; + } catch (\Throwable $e) { + setlocale(LC_NUMERIC, $locale); + throw $e; + } + setlocale(LC_NUMERIC, $locale); + } + + /** + * verify that {{}} are not going to be replaced in parameters. + */ + public function testNoTablenameReplacement() + { + $db = $this->getConnection(); + + $db->createCommand()->insert( + '{{customer}}', + [ + 'name' => 'Some {{weird}} name', + 'email' => 'test@example.com', + 'address' => 'Some {{%weird}} address', + ] + )->execute(); + + $customerId = $db->getLastInsertID('customer_SEQ'); + + $customer = $db->createCommand('SELECT * FROM {{customer}} WHERE [[id]]=' . $customerId)->queryOne(); + $this->assertEquals('Some {{weird}} name', $customer['name']); + $this->assertEquals('Some {{%weird}} address', $customer['address']); + + $db->createCommand()->update( + '{{customer}}', + [ + 'name' => 'Some {{updated}} name', + 'address' => 'Some {{%updated}} address', + ], + ['id' => $customerId] + )->execute(); + $customer = $db->createCommand('SELECT * FROM {{customer}} WHERE [[id]]=' . $customerId)->queryOne(); + $this->assertEquals('Some {{updated}} name', $customer['name']); + $this->assertEquals('Some {{%updated}} address', $customer['address']); + } + + public function testCreateTable() + { + $db = $this->getConnection(); + + if ($db->getSchema()->getTableSchema("testCreateTable") !== null) { + $db->createCommand("DROP SEQUENCE testCreateTable_SEQ")->execute(); + $db->createCommand()->dropTable("testCreateTable")->execute(); + } + + $db->createCommand()->createTable( + '{{testCreateTable}}', + ['id' => Schema::TYPE_PK, 'bar' => Schema::TYPE_INTEGER] + )->execute(); + + $db->createCommand('CREATE SEQUENCE testCreateTable_SEQ START with 1 INCREMENT BY 1')->execute(); + + $db->createCommand( + 'INSERT INTO {{testCreateTable}} ("id", "bar") VALUES(testCreateTable_SEQ.NEXTVAL, 1)' + )->execute(); + + $records = $db->createCommand('SELECT [[id]], [[bar]] FROM {{testCreateTable}}')->queryAll(); + + $this->assertEquals([ + ['id' => 1, 'bar' => 1], + ], $records); + } + + public function testsInsertQueryAsColumnValue() + { + $time = time(); + + $db = $this->getConnection(); + $db->createCommand('DELETE FROM {{order_with_null_fk}}')->execute(); + + $command = $db->createCommand(); + $command->insert('{{order}}', [ + 'customer_id' => 1, + 'created_at' => $time, + 'total' => 42, + ])->execute(); + + $orderId = $db->getLastInsertID('order_SEQ'); + + $columnValueQuery = new \yii\db\Query(); + $columnValueQuery->select('created_at')->from('{{order}}')->where(['id' => $orderId]); + + $command = $db->createCommand(); + $command->insert( + '{{order_with_null_fk}}', + [ + 'customer_id' => $orderId, + 'created_at' => $columnValueQuery, + 'total' => 42, + ] + )->execute(); + + $this->assertEquals($time, $db->createCommand( + 'SELECT [[created_at]] FROM {{order_with_null_fk}} WHERE [[customer_id]] = ' . $orderId + )->queryScalar()); + + $db->createCommand('DELETE FROM {{order_with_null_fk}}')->execute(); + $db->createCommand('DELETE FROM {{order}} WHERE [[id]] = ' . $orderId)->execute(); + } + + public function testAlterTable() + { + $db = $this->getConnection(); + + if ($db->getSchema()->getTableSchema('testAlterTable') !== null) { + $db->createCommand("DROP SEQUENCE testAlterTable_SEQ")->execute(); + $db->createCommand()->dropTable('testAlterTable')->execute(); + } + + $db->createCommand()->createTable( + 'testAlterTable', + ['id' => Schema::TYPE_PK, 'bar' => Schema::TYPE_INTEGER] + )->execute(); + + $db->createCommand('CREATE SEQUENCE testAlterTable_SEQ START with 1 INCREMENT BY 1')->execute(); + + $db->createCommand( + 'INSERT INTO {{testAlterTable}} ([[id]], [[bar]]) VALUES(testAlterTable_SEQ.NEXTVAL, 1)' + )->execute(); + + $db->createCommand('ALTER TABLE {{testAlterTable}} ADD ([[bar_tmp]] VARCHAR(20))')->execute(); + + $db->createCommand('UPDATE {{testAlterTable}} SET [[bar_tmp]] = [[bar]]')->execute(); + + $db->createCommand('ALTER TABLE {{testAlterTable}} DROP COLUMN [[bar]]')->execute(); + + $db->createCommand('ALTER TABLE {{testAlterTable}} RENAME COLUMN [[bar_tmp]] TO [[bar]]')->execute(); + + $db->createCommand( + "INSERT INTO {{testAlterTable}} ([[id]], [[bar]]) VALUES(testAlterTable_SEQ.NEXTVAL, 'hello')" + )->execute(); + + $records = $db->createCommand('SELECT [[id]], [[bar]] FROM {{testAlterTable}}')->queryAll(); + + $this->assertEquals([ + ['id' => 1, 'bar' => 1], + ['id' => 2, 'bar' => 'hello'], + ], $records); + } + + public function testCreateView() + { + $db = $this->getConnection(); + + $subquery = (new Query()) + ->select('bar') + ->from('testCreateViewTable') + ->where(['>', 'bar', '5']); + + if ($db->getSchema()->getTableSchema('testCreateView') !== null) { + $db->createCommand()->dropView('testCreateView')->execute(); + } + + if ($db->getSchema()->getTableSchema('testCreateViewTable')) { + $db->createCommand("DROP SEQUENCE testCreateViewTable_SEQ")->execute(); + $db->createCommand()->dropTable('testCreateViewTable')->execute(); + } + + $db->createCommand()->createTable('testCreateViewTable', [ + 'id' => Schema::TYPE_PK, + 'bar' => Schema::TYPE_INTEGER, + ])->execute(); + + $db->createCommand('CREATE SEQUENCE testCreateViewTable_SEQ START with 1 INCREMENT BY 1')->execute(); + + $db->createCommand( + 'INSERT INTO {{testCreateViewTable}} ("id", "bar") VALUES(testCreateTable_SEQ.NEXTVAL, 1)' + )->execute(); + + $db->createCommand( + 'INSERT INTO {{testCreateViewTable}} ("id", "bar") VALUES(testCreateTable_SEQ.NEXTVAL, 6)' + )->execute(); + + $db->createCommand()->createView('testCreateView', $subquery)->execute(); + + $records = $db->createCommand('SELECT [[bar]] FROM {{testCreateView}}')->queryAll(); + + $this->assertEquals([['bar' => 6]], $records); + } + + public function testColumnCase() + { + $this->markTestSkipped('Should be fixed.'); + } } diff --git a/tests/framework/db/oci/ConnectionTest.php b/tests/framework/db/oci/ConnectionTest.php index 95b00fe..d4aef99 100644 --- a/tests/framework/db/oci/ConnectionTest.php +++ b/tests/framework/db/oci/ConnectionTest.php @@ -7,6 +7,7 @@ namespace yiiunit\framework\db\oci; +use yii\db\Connection; use yii\db\Transaction; /** @@ -85,4 +86,35 @@ class ConnectionTest extends \yiiunit\framework\db\ConnectionTest $transaction = $connection->beginTransaction(Transaction::SERIALIZABLE); $transaction->commit(); } + + /** + * Note: The READ UNCOMMITTED isolation level allows dirty reads. Oracle Database doesn't use dirty reads, nor does + * it even allow them. + * + * Change Transaction::READ_UNCOMMITTED => Transaction::READ_COMMITTED. + */ + public function testTransactionShortcutCustom() + { + $connection = $this->getConnection(true); + + $result = $connection->transaction(static function (Connection $db) { + $db->createCommand()->insert('profile', ['description' => 'test transaction shortcut'])->execute(); + return true; + }, Transaction::READ_COMMITTED); + + $this->assertTrue($result, 'transaction shortcut valid value should be returned from callback'); + + $profilesCount = $connection->createCommand( + "SELECT COUNT(*) FROM {{profile}} WHERE [[description]] = 'test transaction shortcut'" + )->queryScalar(); + $this->assertEquals(1, $profilesCount, 'profile should be inserted in transaction shortcut'); + } + + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } } diff --git a/tests/framework/db/oci/QueryBuilderTest.php b/tests/framework/db/oci/QueryBuilderTest.php index 10e1f16..daed2d1 100644 --- a/tests/framework/db/oci/QueryBuilderTest.php +++ b/tests/framework/db/oci/QueryBuilderTest.php @@ -9,6 +9,7 @@ namespace yiiunit\framework\db\oci; use yii\db\oci\QueryBuilder; use yii\db\oci\Schema; +use yii\helpers\ArrayHelper; use yiiunit\data\base\TraversableObject; /** @@ -75,6 +76,10 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest return $result; } + /** + * @dataProvider defaultValuesProvider + * @param string $sql + */ public function testAddDropDefaultValue($sql, \Closure $builder) { $this->markTestSkipped('Adding/dropping default constraints is not supported in Oracle.'); @@ -137,11 +142,14 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest return parent::likeConditionProvider(); } - public function conditionProvider() + public function conditionProvidertmp() { - return array_merge(parent::conditionProvider(), [ + // test bc with commit + // {@see https://github.com/yiisoft/yii2/commit/d16586334d7bea226a67aa8db28982848b5c92dd#diff-ae95e8cbf4e036860dd6b41011f9f8035a616a8f45d3c3167b3705d39879c95c} + // should be fixed. + return array_merge([], [ [ - ['in', 'id', range(0, 2500)], + ['in', '[[id]]', range(0, 2500)], ' (' . '([[id]] IN (' . implode(', ', $this->generateSprintfSeries(':qp%d', 0, 999)) . '))' @@ -152,7 +160,7 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest array_flip($this->generateSprintfSeries(':qp%d', 0, 2500)), ], [ - ['not in', 'id', range(0, 2500)], + ['not in', '[[id]]', range(0, 2500)], '(' . '([[id]] NOT IN (' . implode(', ', $this->generateSprintfSeries(':qp%d', 0, 999)) . '))' @@ -163,7 +171,7 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest array_flip($this->generateSprintfSeries(':qp%d', 0, 2500)), ], [ - ['not in', 'id', new TraversableObject(range(0, 2500))], + ['not in', '[[id]]', new TraversableObject(range(0, 2500))], '(' . '([[id]] NOT IN (' . implode(', ', $this->generateSprintfSeries(':qp%d', 0, 999)) . '))' @@ -203,21 +211,21 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) SELECT * FROM PAGINATION -WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "status"="EXCLUDED"."status" WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")', +WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "status"="EXCLUDED"."status" WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")' ], 'query with update part' => [ 3 => 'MERGE INTO "T_upsert" USING (WITH USER_SQL AS (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0), PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) SELECT * FROM PAGINATION -WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"=:qp1, "status"=:qp2, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")', +WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"=:qp1, "status"=:qp2, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")' ], 'query without update part' => [ 3 => 'MERGE INTO "T_upsert" USING (WITH USER_SQL AS (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0), PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) SELECT * FROM PAGINATION -WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")', +WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")' ], 'values and expressions' => [ 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', @@ -245,4 +253,65 @@ WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN NO return $newData; } + + public function batchInsertProvider() + { + $data = parent::batchInsertProvider(); + + $data[0][3] = 'INSERT ALL INTO "customer" ("email", "name", "address") ' . + "VALUES ('test@example.com', 'silverfire', 'Kyiv {{city}}, Ukraine') SELECT 1 FROM SYS.DUAL"; + + $data['escape-danger-chars']['expected'] = 'INSERT ALL INTO "customer" ("address") ' . + "VALUES ('SQL-danger chars are escaped: ''); --') SELECT 1 FROM SYS.DUAL"; + + $data[2][3] = 'INSERT ALL INTO "customer" () ' . + "VALUES ('no columns passed') SELECT 1 FROM SYS.DUAL"; + + $data['bool-false, bool2-null']['expected'] = 'INSERT ALL INTO "type" ("bool_col", "bool_col2") ' . + "VALUES ('', NULL) SELECT 1 FROM SYS.DUAL"; + + $data[3][3] = 'INSERT ALL INTO {{%type}} ({{%type}}.[[float_col]], [[time]]) ' . + "VALUES (NULL, now()) SELECT 1 FROM SYS.DUAL"; + + $data['bool-false, time-now()']['expected'] = 'INSERT ALL INTO {{%type}} ({{%type}}.[[bool_col]], [[time]]) ' . + "VALUES (0, now()) SELECT 1 FROM SYS.DUAL"; + + return $data; + } + + /** + * Dummy test to speed up QB's tests which rely on DB schema + */ + public function testInitFixtures() + { + $this->assertInstanceOf('yii\db\QueryBuilder', $this->getQueryBuilder(true, true)); + } + + /** + * @depends testInitFixtures + * @dataProvider upsertProvider + * @param string $table + * @param array $insertColumns + * @param array|null $updateColumns + * @param string|string[] $expectedSQL + * @param array $expectedParams + * @throws \yii\base\NotSupportedException + * @throws \Exception + */ + public function testUpsert($table, $insertColumns, $updateColumns, $expectedSQL, $expectedParams) + { + $actualParams = []; + $actualSQL = $this->getQueryBuilder(true, $this->driverName === 'sqlite')->upsert($table, $insertColumns, $updateColumns, $actualParams); + if (is_string($expectedSQL)) { + $this->assertEqualsWithoutLE($expectedSQL, $actualSQL); + } else { + $this->assertContains($actualSQL, $expectedSQL); + } + if (ArrayHelper::isAssociative($expectedParams)) { + $this->assertSame($expectedParams, $actualParams); + } else { + $this->assertIsOneOf($actualParams, $expectedParams); + } + } + } diff --git a/tests/framework/db/oci/QueryTest.php b/tests/framework/db/oci/QueryTest.php index e2a2c82..d5acd3c 100644 --- a/tests/framework/db/oci/QueryTest.php +++ b/tests/framework/db/oci/QueryTest.php @@ -7,8 +7,6 @@ namespace yiiunit\framework\db\oci; -use yii\db\Query; - /** * @group db * @group oci @@ -17,14 +15,8 @@ class QueryTest extends \yiiunit\framework\db\QueryTest { protected $driverName = 'oci'; - public function testOne() + public function testUnion() { - $db = $this->getConnection(); - - $result = (new Query())->from('customer')->where(['[[status]]' => 2])->one($db); - $this->assertEquals('user3', $result['name']); - - $result = (new Query())->from('customer')->where(['[[status]]' => 3])->one($db); - $this->assertFalse($result); + $this->markTestSkipped('Unsupported use of WITH clause in Oracle.'); } } diff --git a/tests/framework/db/oci/SchemaTest.php b/tests/framework/db/oci/SchemaTest.php index 1541d06..b0335b3 100644 --- a/tests/framework/db/oci/SchemaTest.php +++ b/tests/framework/db/oci/SchemaTest.php @@ -43,6 +43,7 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest $columns['smallint_col']['size'] = 22; $columns['smallint_col']['precision'] = null; $columns['smallint_col']['scale'] = 0; + $columns['char_col']['type'] = 'string'; $columns['char_col']['dbType'] = 'CHAR'; $columns['char_col']['precision'] = null; $columns['char_col']['size'] = 100; @@ -174,4 +175,61 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest ]); return $result; } + + public function testFindUniqueIndexes() + { + if ($this->driverName === 'sqlsrv') { + $this->markTestSkipped('`\yii\db\mssql\Schema::findUniqueIndexes()` returns only unique constraints not unique indexes.'); + } + + $db = $this->getConnection(); + + try { + $db->createCommand()->dropTable('uniqueIndex')->execute(); + } catch (\Exception $e) { + } + $db->createCommand()->createTable('uniqueIndex', [ + 'somecol' => 'string', + 'someCol2' => 'string', + 'someCol3' => 'string', + ])->execute(); + + /* @var $schema Schema */ + $schema = $db->schema; + + $uniqueIndexes = $schema->findUniqueIndexes($schema->getTableSchema('uniqueIndex', true)); + $this->assertEquals([], $uniqueIndexes); + + $db->createCommand()->createIndex('somecolUnique', 'uniqueIndex', 'somecol', true)->execute(); + + $uniqueIndexes = $schema->findUniqueIndexes($schema->getTableSchema('uniqueIndex', true)); + $this->assertEquals([ + 'somecolUnique' => ['somecol'], + ], $uniqueIndexes); + + // create another column with upper case letter that fails postgres + // see https://github.com/yiisoft/yii2/issues/10613 + $db->createCommand()->createIndex('someCol2Unique', 'uniqueIndex', 'someCol2', true)->execute(); + + $uniqueIndexes = $schema->findUniqueIndexes($schema->getTableSchema('uniqueIndex', true)); + $this->assertEquals([ + 'somecolUnique' => ['somecol'], + 'someCol2Unique' => ['someCol2'], + ], $uniqueIndexes); + + // see https://github.com/yiisoft/yii2/issues/13814 + $db->createCommand()->createIndex('another unique index', 'uniqueIndex', 'someCol3', true)->execute(); + + $uniqueIndexes = $schema->findUniqueIndexes($schema->getTableSchema('uniqueIndex', true)); + $this->assertEquals([ + 'somecolUnique' => ['somecol'], + 'someCol2Unique' => ['someCol2'], + 'another unique index' => ['someCol3'], + ], $uniqueIndexes); + } + + public function testCompositeFk() + { + $this->markTestSkipped('Should be fixed.'); + } } diff --git a/tests/framework/db/oci/UniqueValidatorTest.php b/tests/framework/db/oci/UniqueValidatorTest.php index 6c313ad..2ce07de 100644 --- a/tests/framework/db/oci/UniqueValidatorTest.php +++ b/tests/framework/db/oci/UniqueValidatorTest.php @@ -7,6 +7,10 @@ namespace yiiunit\framework\db\oci; +use yii\validators\UniqueValidator; +use yiiunit\data\validators\models\ValidatorTestMainModel; +use yiiunit\data\validators\models\ValidatorTestRefModel; + /** * @group db * @group oci @@ -15,4 +19,21 @@ namespace yiiunit\framework\db\oci; class UniqueValidatorTest extends \yiiunit\framework\validators\UniqueValidatorTest { public $driverName = 'oci'; + + public function testValidateEmptyAttributeInStringField() + { + ValidatorTestMainModel::deleteAll(); + + $val = new UniqueValidator(); + + $m = new ValidatorTestMainModel(['id' => 5, 'field1' => ' ']); + + $val->validateAttribute($m, 'field1'); + $this->assertFalse($m->hasErrors('field1')); + $m->save(false); + + $m = new ValidatorTestMainModel(['field1' => ' ']); + $val->validateAttribute($m, 'field1'); + $this->assertTrue($m->hasErrors('field1')); + } } diff --git a/tests/framework/db/pgsql/QueryBuilderTest.php b/tests/framework/db/pgsql/QueryBuilderTest.php index 14b2eaf..a5cc6f2 100644 --- a/tests/framework/db/pgsql/QueryBuilderTest.php +++ b/tests/framework/db/pgsql/QueryBuilderTest.php @@ -185,6 +185,10 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest $expected = 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ALTER COLUMN "bar" DROP DEFAULT, ALTER COLUMN "bar" DROP NOT NULL, ADD CONSTRAINT foo1_bar_check CHECK (char_length(bar) > 5)'; $sql = $qb->alterColumn('foo1', 'bar', $this->string(255)->check('char_length(bar) > 5')); $this->assertEquals($expected, $sql); + + $expected = 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ALTER COLUMN "bar" SET DEFAULT \'\', ALTER COLUMN "bar" DROP NOT NULL'; + $sql = $qb->alterColumn('foo1', 'bar', $this->string(255)->defaultValue('')); + $this->assertEquals($expected, $sql); $expected = 'ALTER TABLE "foo1" ALTER COLUMN "bar" TYPE varchar(255), ALTER COLUMN "bar" SET DEFAULT \'AbCdE\', ALTER COLUMN "bar" DROP NOT NULL'; $sql = $qb->alterColumn('foo1', 'bar', $this->string(255)->defaultValue('AbCdE')); @@ -261,6 +265,27 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest $this->assertEquals($expected, $sql); } + public function testResetSequencePostgres12() + { + if (version_compare($this->getConnection(false)->getServerVersion(), '12.0', '<')) { + $this->markTestSkipped('PostgreSQL < 12.0 does not support GENERATED AS IDENTITY columns.'); + } + + $config = $this->database; + unset($config['fixture']); + $this->prepareDatabase($config, realpath(__DIR__.'/../../../data') . '/postgres12.sql'); + + $qb = $this->getQueryBuilder(false); + + $expected = "SELECT SETVAL('\"item_12_id_seq\"',(SELECT COALESCE(MAX(\"id\"),0) FROM \"item_12\")+1,false)"; + $sql = $qb->resetSequence('item_12'); + $this->assertEquals($expected, $sql); + + $expected = "SELECT SETVAL('\"item_12_id_seq\"',4,false)"; + $sql = $qb->resetSequence('item_12', 4); + $this->assertEquals($expected, $sql); + } + public function upsertProvider() { $concreteData = [ diff --git a/tests/framework/db/pgsql/SchemaTest.php b/tests/framework/db/pgsql/SchemaTest.php index d919759..d3b0ad3 100644 --- a/tests/framework/db/pgsql/SchemaTest.php +++ b/tests/framework/db/pgsql/SchemaTest.php @@ -75,7 +75,9 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest $columns['bool_col2']['precision'] = null; $columns['bool_col2']['scale'] = null; $columns['bool_col2']['defaultValue'] = true; - $columns['ts_default']['defaultValue'] = new Expression('now()'); + if (version_compare($this->getConnection(false)->getServerVersion(), '10', '<')) { + $columns['ts_default']['defaultValue'] = new Expression('now()'); + } $columns['bit_col']['dbType'] = 'bit'; $columns['bit_col']['size'] = 8; $columns['bit_col']['precision'] = null; @@ -204,6 +206,52 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest $this->assertFalse($table->getColumn('default_false')->defaultValue); } + public function testSequenceName() + { + $connection = $this->getConnection(); + + $sequenceName = $connection->schema->getTableSchema('item')->sequenceName; + + $connection->createCommand('ALTER TABLE "item" ALTER COLUMN "id" SET DEFAULT nextval(\'item_id_seq_2\')')->execute(); + + $connection->schema->refreshTableSchema('item'); + $this->assertEquals('item_id_seq_2', $connection->schema->getTableSchema('item')->sequenceName); + + $connection->createCommand('ALTER TABLE "item" ALTER COLUMN "id" SET DEFAULT nextval(\'' . $sequenceName . '\')')->execute(); + $connection->schema->refreshTableSchema('item'); + $this->assertEquals($sequenceName, $connection->schema->getTableSchema('item')->sequenceName); + } + + public function testGeneratedValues() + { + if (version_compare($this->getConnection(false)->getServerVersion(), '12.0', '<')) { + $this->markTestSkipped('PostgreSQL < 12.0 does not support GENERATED AS IDENTITY columns.'); + } + + $config = $this->database; + unset($config['fixture']); + $this->prepareDatabase($config, realpath(__DIR__.'/../../../data') . '/postgres12.sql'); + + $table = $this->getConnection(false)->schema->getTableSchema('generated'); + $this->assertTrue($table->getColumn('id_always')->autoIncrement); + $this->assertTrue($table->getColumn('id_primary')->autoIncrement); + $this->assertTrue($table->getColumn('id_primary')->isPrimaryKey); + $this->assertTrue($table->getColumn('id_default')->autoIncrement); + } + + public function testPartitionedTable() + { + if (version_compare($this->getConnection(false)->getServerVersion(), '10.0', '<')) { + $this->markTestSkipped('PostgreSQL < 10.0 does not support PARTITION BY clause.'); + } + + $config = $this->database; + unset($config['fixture']); + $this->prepareDatabase($config, realpath(__DIR__.'/../../../data') . '/postgres10.sql'); + + $this->assertNotNull($this->getConnection(false)->schema->getTableSchema('partitioned')); + } + public function testFindSchemaNames() { $schema = $this->getConnection()->schema; @@ -295,7 +343,7 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest public function constraintsProvider() { $result = parent::constraintsProvider(); - $result['1: check'][2][0]->expression = '(("C_check")::text <> \'\'::text)'; + $result['1: check'][2][0]->expression = 'CHECK ((("C_check")::text <> \'\'::text))'; $result['3: foreign key'][2][0]->foreignSchemaName = 'public'; $result['3: index'][2] = []; diff --git a/tests/framework/db/sqlite/QueryBuilderTest.php b/tests/framework/db/sqlite/QueryBuilderTest.php index e0dd672..084674f 100644 --- a/tests/framework/db/sqlite/QueryBuilderTest.php +++ b/tests/framework/db/sqlite/QueryBuilderTest.php @@ -9,6 +9,7 @@ namespace yiiunit\framework\db\sqlite; use yii\db\Query; use yii\db\Schema; +use yii\db\sqlite\QueryBuilder; use yiiunit\data\base\TraversableObject; /** @@ -65,6 +66,18 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest { $result = parent::indexesProvider(); $result['drop'][0] = 'DROP INDEX [[CN_constraints_2_single]]'; + + $indexName = 'myindex'; + $schemaName = 'myschema'; + $tableName = 'mytable'; + + $result['with schema'] = [ + "CREATE INDEX {{{$schemaName}}}.[[$indexName]] ON {{{$tableName}}} ([[C_index_1]])", + function (QueryBuilder $qb) use ($tableName, $indexName, $schemaName) { + return $qb->createIndex($indexName, $schemaName . '.' . $tableName, 'C_index_1'); + }, + ]; + return $result; } @@ -143,6 +156,37 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest $this->assertEquals([], $queryParams); } + public function testBuildWithQuery() + { + $expectedQuerySql = $this->replaceQuotes( + 'WITH a1 AS (SELECT [[id]] FROM [[t1]] WHERE expr = 1), a2 AS (SELECT [[id]] FROM [[t2]] INNER JOIN [[a1]] ON t2.id = a1.id WHERE expr = 2 UNION SELECT [[id]] FROM [[t3]] WHERE expr = 3) SELECT * FROM [[a2]]' + ); + $with1Query = (new Query()) + ->select('id') + ->from('t1') + ->where('expr = 1'); + + $with2Query = (new Query()) + ->select('id') + ->from('t2') + ->innerJoin('a1', 't2.id = a1.id') + ->where('expr = 2'); + + $with3Query = (new Query()) + ->select('id') + ->from('t3') + ->where('expr = 3'); + + $query = (new Query()) + ->withQuery($with1Query, 'a1') + ->withQuery($with2Query->union($with3Query), 'a2') + ->from('a2'); + + list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals([], $queryParams); + } + public function testResetSequence() { $qb = $this->getQueryBuilder(true, true); diff --git a/tests/framework/di/ContainerTest.php b/tests/framework/di/ContainerTest.php index 291e86a..7eb0f10 100644 --- a/tests/framework/di/ContainerTest.php +++ b/tests/framework/di/ContainerTest.php @@ -14,12 +14,19 @@ use yii\validators\NumberValidator; use yiiunit\data\ar\Cat; use yiiunit\data\ar\Order; use yiiunit\data\ar\Type; +use yiiunit\framework\di\stubs\Alpha; use yiiunit\framework\di\stubs\Bar; use yiiunit\framework\di\stubs\BarSetter; +use yiiunit\framework\di\stubs\Beta; +use yiiunit\framework\di\stubs\Car; +use yiiunit\framework\di\stubs\Corge; use yiiunit\framework\di\stubs\Foo; use yiiunit\framework\di\stubs\FooProperty; +use yiiunit\framework\di\stubs\Kappa; use yiiunit\framework\di\stubs\Qux; use yiiunit\framework\di\stubs\QuxInterface; +use yiiunit\framework\di\stubs\QuxFactory; +use yiiunit\framework\di\stubs\Zeta; use yiiunit\TestCase; /** @@ -170,7 +177,7 @@ class ContainerTest extends TestCase $myFunc = function ($a, NumberValidator $b, $c = 'default') { - return[$a, \get_class($b), $c]; + return [$a, \get_class($b), $c]; }; $result = Yii::$container->invoke($myFunc, ['a']); $this->assertEquals(['a', 'yii\validators\NumberValidator', 'default'], $result); @@ -260,7 +267,8 @@ class ContainerTest extends TestCase 'qux.using.closure' => function () { return new Qux(); }, - 'rollbar', 'baibaratsky\yii\rollbar\Rollbar' + 'rollbar', + 'baibaratsky\yii\rollbar\Rollbar' ]); $container->setDefinitions([]); @@ -276,12 +284,170 @@ class ContainerTest extends TestCase try { $container->get('rollbar'); $this->fail('InvalidConfigException was not thrown'); - } catch(\Exception $e) - { + } catch (\Exception $e) { $this->assertInstanceOf('yii\base\InvalidConfigException', $e); } } + public function testStaticCall() + { + $container = new Container(); + $container->setDefinitions([ + 'qux' => [QuxFactory::className(), 'create'], + ]); + + $qux = $container->get('qux'); + $this->assertInstanceOf(Qux::className(), $qux); + $this->assertSame(42, $qux->a); + } + + public function testObject() + { + $container = new Container(); + $container->setDefinitions([ + 'qux' => new Qux(42), + ]); + + $qux = $container->get('qux'); + $this->assertInstanceOf(Qux::className(), $qux); + $this->assertSame(42, $qux->a); + } + + public function testDi3Compatibility() + { + $container = new Container(); + $container->setDefinitions([ + 'test\TraversableInterface' => [ + '__class' => 'yiiunit\data\base\TraversableObject', + '__construct()' => [['item1', 'item2']], + ], + 'qux' => [ + '__class' => Qux::className(), + 'a' => 42, + ], + ]); + + $qux = $container->get('qux'); + $this->assertInstanceOf(Qux::className(), $qux); + $this->assertSame(42, $qux->a); + + $traversable = $container->get('test\TraversableInterface'); + $this->assertInstanceOf('yiiunit\data\base\TraversableObject', $traversable); + $this->assertEquals('item1', $traversable->current()); + } + + public function testInstanceOf() + { + $container = new Container(); + $container->setDefinitions([ + 'qux' => [ + 'class' => Qux::className(), + 'a' => 42, + ], + 'bar' => [ + '__class' => Bar::className(), + '__construct()' => [ + Instance::of('qux') + ], + ], + ]); + $bar = $container->get('bar'); + $this->assertInstanceOf(Bar::className(), $bar); + $qux = $bar->qux; + $this->assertInstanceOf(Qux::className(), $qux); + $this->assertSame(42, $qux->a); + } + + public function testReferencesInArrayInDependencies() + { + $quxInterface = 'yiiunit\framework\di\stubs\QuxInterface'; + $container = new Container(); + $container->resolveArrays = true; + $container->setSingletons([ + $quxInterface => [ + 'class' => Qux::className(), + 'a' => 42, + ], + 'qux' => Instance::of($quxInterface), + 'bar' => [ + 'class' => Bar::className(), + ], + 'corge' => [ + '__class' => Corge::className(), + '__construct()' => [ + [ + 'qux' => Instance::of('qux'), + 'bar' => Instance::of('bar'), + 'q33' => new Qux(33), + ], + ], + ], + ]); + $corge = $container->get('corge'); + $this->assertInstanceOf(Corge::className(), $corge); + $qux = $corge->map['qux']; + $this->assertInstanceOf(Qux::className(), $qux); + $this->assertSame(42, $qux->a); + $bar = $corge->map['bar']; + $this->assertInstanceOf(Bar::className(), $bar); + $this->assertSame($qux, $bar->qux); + $q33 = $corge->map['q33']; + $this->assertInstanceOf(Qux::className(), $q33); + $this->assertSame(33, $q33->a); + } + + public function testGetByInstance() + { + $container = new Container(); + $container->setSingletons([ + 'one' => Qux::className(), + 'two' => Instance::of('one'), + ]); + $one = $container->get(Instance::of('one')); + $two = $container->get(Instance::of('two')); + $this->assertInstanceOf(Qux::className(), $one); + $this->assertSame($one, $two); + $this->assertSame($one, $container->get('one')); + $this->assertSame($one, $container->get('two')); + } + + public function testWithoutDefinition() + { + $container = new Container(); + + $one = $container->get(Qux::className()); + $two = $container->get(Qux::className()); + $this->assertInstanceOf(Qux::className(), $one); + $this->assertInstanceOf(Qux::className(), $two); + $this->assertSame(1, $one->a); + $this->assertSame(1, $two->a); + $this->assertNotSame($one, $two); + } + + public function testGetByClassIndirectly() + { + $container = new Container(); + $container->setSingletons([ + 'qux' => Qux::className(), + Qux::className() => [ + 'a' => 42, + ], + ]); + + $qux = $container->get('qux'); + $this->assertInstanceOf(Qux::className(), $qux); + $this->assertSame(42, $qux->a); + } + + /** + * @expectedException \yii\base\InvalidConfigException + */ + public function testThrowingNotFoundException() + { + $container = new Container(); + $container->get('non_existing'); + } + public function testContainerSingletons() { $container = new Container(); @@ -334,4 +500,125 @@ class ContainerTest extends TestCase require __DIR__ . '/testContainerWithVariadicCallable.php'; } + + /** + * @see https://github.com/yiisoft/yii2/issues/18245 + */ + public function testDelayedInitializationOfSubArray() + { + $definitions = [ + 'test' => [ + 'class' => Corge::className(), + '__construct()' => [ + [Instance::of('setLater')], + ], + ], + ]; + + $application = Yii::createObject([ + '__class' => \yii\web\Application::className(), + 'basePath' => __DIR__, + 'id' => 'test', + 'components' => [ + 'request' => [ + 'baseUrl' => '123' + ], + ], + 'container' => [ + 'definitions' => $definitions, + ], + ]); + + Yii::$container->set('setLater', new Qux()); + Yii::$container->get('test'); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18304 + */ + public function testNulledConstructorParameters() + { + $alpha = (new Container())->get(Alpha::className()); + $this->assertInstanceOf(Beta::className(), $alpha->beta); + $this->assertNull($alpha->omega); + + $QuxInterface = __NAMESPACE__ . '\stubs\QuxInterface'; + $container = new Container(); + $container->set($QuxInterface, Qux::className()); + $alpha = $container->get(Alpha::className()); + $this->assertInstanceOf(Beta::className(), $alpha->beta); + $this->assertInstanceOf($QuxInterface, $alpha->omega); + $this->assertNull($alpha->unknown); + $this->assertNull($alpha->color); + + $container = new Container(); + $container->set(__NAMESPACE__ . '\stubs\AbstractColor', __NAMESPACE__ . '\stubs\Color'); + $alpha = $container->get(Alpha::className()); + $this->assertInstanceOf(__NAMESPACE__ . '\stubs\Color', $alpha->color); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18284 + */ + public function testNamedConstructorParameters() + { + $test = (new Container())->get(Car::className(), [ + 'name' => 'Hello', + 'color' => 'red', + ]); + $this->assertSame('Hello', $test->name); + $this->assertSame('red', $test->color); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18284 + */ + public function testInvalidConstructorParameters() + { + $this->expectException('yii\base\InvalidConfigException'); + $this->expectExceptionMessage('Dependencies indexed by name and by position in the same array are not allowed.'); + (new Container())->get(Car::className(), [ + 'color' => 'red', + 'Hello', + ]); + } + + public function dataNotInstantiableException() + { + return [ + [Bar::className()], + [Kappa::className()], + ]; + } + + /** + * @dataProvider dataNotInstantiableException + * + * @see https://github.com/yiisoft/yii2/pull/18379 + * + * @param string $class + */ + public function testNotInstantiableException($class) + { + $this->expectException('yii\di\NotInstantiableException'); + (new Container())->get($class); + } + + public function testNullTypeConstructorParameters() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + + $zeta = (new Container())->get(Zeta::className()); + $this->assertInstanceOf(Beta::className(), $zeta->beta); + $this->assertInstanceOf(Beta::className(), $zeta->betaNull); + $this->assertNull($zeta->color); + $this->assertNull($zeta->colorNull); + $this->assertNull($zeta->qux); + $this->assertNull($zeta->quxNull); + $this->assertNull($zeta->unknown); + $this->assertNull($zeta->unknownNull); + } } diff --git a/tests/framework/di/InstanceTest.php b/tests/framework/di/InstanceTest.php index e93b173..a5b23c5 100644 --- a/tests/framework/di/InstanceTest.php +++ b/tests/framework/di/InstanceTest.php @@ -171,12 +171,7 @@ class InstanceTest extends TestCase $instance = Instance::of('something'); $export = var_export($instance, true); - $this->assertRegExp(<<<'PHP' -@yii\\di\\Instance::__set_state\(array\( -\s+'id' => 'something', -\)\)@ -PHP - , $export); + $this->assertRegExp('~yii\\\\di\\\\Instance::__set_state\(array\(\s+\'id\' => \'something\',\s+\'optional\' => false,\s+\)\)~', $export); $this->assertEquals($instance, Instance::__set_state([ 'id' => 'something', diff --git a/tests/framework/di/ServiceLocatorTest.php b/tests/framework/di/ServiceLocatorTest.php index b3d19dd..1a49a9f 100644 --- a/tests/framework/di/ServiceLocatorTest.php +++ b/tests/framework/di/ServiceLocatorTest.php @@ -25,6 +25,10 @@ class TestClass extends BaseObject public $prop2; } +class TestSubclass extends TestClass +{ +} + /** * @author Qiang Xue * @since 2.0 @@ -67,6 +71,24 @@ class ServiceLocatorTest extends TestCase $this->assertSame($container->get($className), $object); } + public function testDi3Compatibility() + { + $config = [ + 'components' => [ + 'test' => [ + 'class' => TestClass::className(), + ], + ], + ]; + + // User Defined Config + $config['components']['test']['__class'] = TestSubclass::className(); + + $app = new ServiceLocator($config); + $this->assertInstanceOf(TestSubclass::className(), $app->get('test')); + } + + public function testShared() { // with configuration: shared diff --git a/tests/framework/di/stubs/AbstractColor.php b/tests/framework/di/stubs/AbstractColor.php new file mode 100644 index 0000000..f3d03a4 --- /dev/null +++ b/tests/framework/di/stubs/AbstractColor.php @@ -0,0 +1,9 @@ +beta = $beta; + $this->omega = $omega; + $this->unknown = $unknown; + $this->color = $color; + } +} diff --git a/tests/framework/di/stubs/Beta.php b/tests/framework/di/stubs/Beta.php new file mode 100644 index 0000000..cf2daa6 --- /dev/null +++ b/tests/framework/di/stubs/Beta.php @@ -0,0 +1,9 @@ +color = $color; + $this->name = $name; + } +} diff --git a/tests/framework/di/stubs/Color.php b/tests/framework/di/stubs/Color.php new file mode 100644 index 0000000..b9bd0f9 --- /dev/null +++ b/tests/framework/di/stubs/Color.php @@ -0,0 +1,7 @@ +map = $map; + parent::__construct($config); + } +} diff --git a/tests/framework/di/stubs/FooBaz.php b/tests/framework/di/stubs/FooBaz.php new file mode 100644 index 0000000..f33603e --- /dev/null +++ b/tests/framework/di/stubs/FooBaz.php @@ -0,0 +1,32 @@ + + * @since 2.0.31 + */ +class FooBaz extends \yii\base\BaseObject +{ + public $fooDependent = []; + + public function init() + { + // default config usually used by Yii + $dependentConfig = array_merge(['class' => FooDependent::className()], $this->fooDependent); + $this->fooDependent = \Yii::createObject($dependentConfig); + } +} + +class FooDependent extends \yii\base\BaseObject +{ +} + +class FooDependentSubclass extends FooDependent +{ +} diff --git a/tests/framework/di/stubs/Kappa.php b/tests/framework/di/stubs/Kappa.php new file mode 100644 index 0000000..21b951b --- /dev/null +++ b/tests/framework/di/stubs/Kappa.php @@ -0,0 +1,12 @@ +beta = $beta; + $this->betaNull = $betaNull; + $this->color = $color; + $this->colorNull = $colorNull; + $this->qux = $qux; + $this->quxNull = $quxNull; + $this->unknown = $unknown; + $this->unknownNull = $unknownNull; + } +} diff --git a/tests/framework/filters/ContentNegotiatorTest.php b/tests/framework/filters/ContentNegotiatorTest.php index c8ecc27..e7a3792 100644 --- a/tests/framework/filters/ContentNegotiatorTest.php +++ b/tests/framework/filters/ContentNegotiatorTest.php @@ -117,4 +117,19 @@ class ContentNegotiatorTest extends TestCase $this->assertContains('Accept', $varyHeader); $this->assertContains('Accept-Language', $varyHeader); } + + public function testNegotiateContentType() + { + $filter = new ContentNegotiator([ + 'formats' => [ + 'application/json' => Response::FORMAT_JSON, + ], + ]); + Yii::$app->request->setAcceptableContentTypes(['application/json' => ['q' => 1, 'version' => '1.0']]); + $filter->negotiate(); + $this->assertSame('json', Yii::$app->response->format); + $this->expectException('\yii\web\NotAcceptableHttpException'); + Yii::$app->request->setAcceptableContentTypes(['application/xml' => ['q' => 1, 'version' => '2.0']]); + $filter->negotiate(); + } } diff --git a/tests/framework/filters/RateLimiterTest.php b/tests/framework/filters/RateLimiterTest.php index 3126176..0b44eb2 100644 --- a/tests/framework/filters/RateLimiterTest.php +++ b/tests/framework/filters/RateLimiterTest.php @@ -7,13 +7,12 @@ namespace yiiunit\framework\filters; -use Prophecy\Argument; use Yii; use yii\filters\RateLimiter; -use yii\log\Logger; use yii\web\Request; use yii\web\Response; use yii\web\User; +use yiiunit\framework\filters\stubs\ExposedLogger; use yiiunit\framework\filters\stubs\RateLimit; use yiiunit\framework\filters\stubs\UserIdentity; use yiiunit\TestCase; @@ -27,15 +26,7 @@ class RateLimiterTest extends TestCase { parent::setUp(); - /* @var $logger Logger|\Prophecy\ObjectProphecy */ - $logger = $this->prophesize(Logger::className()); - $logger - ->log(Argument::any(), Argument::any(), Argument::any()) - ->will(function ($parameters, $logger) { - $logger->messages = $parameters; - }); - - Yii::setLogger($logger->reveal()); + Yii::setLogger(new ExposedLogger()); $this->mockWebApplication(); } @@ -158,4 +149,21 @@ class RateLimiterTest extends TestCase $rateLimiter->addRateLimitHeaders($response, 1, 0, 0); $this->assertCount(3, $response->getHeaders()); } + + /** + * @see https://github.com/yiisoft/yii2/issues/18236 + */ + public function testUserWithClosureFunction() + { + $rateLimiter = new RateLimiter(); + $rateLimiter->user = function($action) { + return new User(['identityClass' => RateLimit::className()]); + }; + $rateLimiter->beforeAction('test'); + + // testing the evaluation of user closure, which in this case returns not the expect object and therefore + // the log message "does not implement RateLimitInterface" is expected. + $this->assertInstanceOf(User::className(), $rateLimiter->user); + $this->assertContains('Rate limit skipped: "user" does not implement RateLimitInterface.', Yii::getLogger()->messages); + } } diff --git a/tests/framework/filters/auth/AuthTest.php b/tests/framework/filters/auth/AuthTest.php index 6ef5634..b0dbe20 100644 --- a/tests/framework/filters/auth/AuthTest.php +++ b/tests/framework/filters/auth/AuthTest.php @@ -29,10 +29,6 @@ class AuthTest extends \yiiunit\TestCase { parent::setUp(); - if (defined('HHVM_VERSION') && getenv('TRAVIS') == 'true') { - $this->markTestSkipped('Can not test reliably with HHVM on travis-ci.'); - } - $_SERVER['SCRIPT_FILENAME'] = '/index.php'; $_SERVER['SCRIPT_NAME'] = '/index.php'; diff --git a/tests/framework/filters/auth/BasicAuthTest.php b/tests/framework/filters/auth/BasicAuthTest.php index d1748fd..55d8ec2 100644 --- a/tests/framework/filters/auth/BasicAuthTest.php +++ b/tests/framework/filters/auth/BasicAuthTest.php @@ -10,6 +10,8 @@ namespace yiiunit\framework\filters\auth; use Yii; use yii\filters\auth\HttpBasicAuth; use yiiunit\framework\filters\stubs\UserIdentity; +use yii\base\Event; +use yii\web\User; /** * @group filters @@ -122,4 +124,20 @@ class BasicAuthTest extends AuthTest ['yii\filters\auth\HttpBasicAuth'], ]; } + + /** + * @dataProvider tokenProvider + * @param string|null $token + * @param string|null $login + */ + public function testAfterLoginEventIsTriggered18031($token, $login) + { + $triggered = false; + Event::on('\yii\web\User', User::EVENT_AFTER_LOGIN, function ($event) use (&$triggered) { + $triggered = true; + $this->assertTrue($triggered); + }); + $this->testHttpBasicAuthCustom($token, $login); + Event::off('\yii\web\User', User::EVENT_AFTER_LOGIN); // required because this method runs in foreach loop. See @dataProvider tokenProvider + } } diff --git a/tests/framework/filters/stubs/ExposedLogger.php b/tests/framework/filters/stubs/ExposedLogger.php new file mode 100644 index 0000000..951ae55 --- /dev/null +++ b/tests/framework/filters/stubs/ExposedLogger.php @@ -0,0 +1,19 @@ +messages[] = $message; + } +} diff --git a/tests/framework/grid/DataColumnTest.php b/tests/framework/grid/DataColumnTest.php index d847a90..487d15d 100644 --- a/tests/framework/grid/DataColumnTest.php +++ b/tests/framework/grid/DataColumnTest.php @@ -8,11 +8,13 @@ namespace yiiunit\framework\grid; use Yii; +use yii\data\ActiveDataProvider; use yii\data\ArrayDataProvider; use yii\grid\DataColumn; use yii\grid\GridView; use yiiunit\data\ar\ActiveRecord; use yiiunit\data\ar\Order; +use yiiunit\data\base\Singer; /** * @author Dmitry Naumenko @@ -95,6 +97,43 @@ class DataColumnTest extends \yiiunit\TestCase $this->assertEquals($result, $filterInput); } + /** + * @see DataColumn::$filter + * @see DataColumn::renderFilterCellContent() + */ + public function testFilterHasMaxLengthWhenIsAnActiveTextInput() + { + $this->mockApplication([ + 'components' => [ + 'db' => [ + 'class' => '\yii\db\Connection', + 'dsn' => 'sqlite::memory:', + ], + ], + ]); + + ActiveRecord::$db = Yii::$app->getDb(); + Yii::$app->getDb()->createCommand()->createTable(Singer::tableName(), [ + 'firstName' => 'string', + 'lastName' => 'string' + ])->execute(); + + $filterInput = ''; + $grid = new GridView([ + 'dataProvider' => new ActiveDataProvider(), + 'filterModel' => new Singer(), + 'columns' => [ + 0 => 'lastName' + ], + ]); + + $dataColumn = $grid->columns[0]; + $method = new \ReflectionMethod($dataColumn, 'renderFilterCellContent'); + $method->setAccessible(true); + $result = $method->invoke($dataColumn); + $this->assertEquals($result, $filterInput); + } + /** * @see DataColumn::$filter @@ -196,4 +235,33 @@ HTML HTML , $result); } + + /** + * @see DataColumn::$filterAttribute + * @see DataColumn::renderFilterCellContent() + */ + public function testFilterInputWithFilterAttribute() + { + $this->mockApplication(); + + $grid = new GridView([ + 'dataProvider' => new ArrayDataProvider([ + 'allModels' => [], + ]), + 'columns' => [ + 0 => [ + 'attribute' => 'username', + 'filterAttribute' => 'user_id', + ], + ], + 'filterModel' => new \yiiunit\data\base\RulesModel(['rules' => [['user_id', 'safe']]]), + ]); + + $dataColumn = $grid->columns[0]; + $method = new \ReflectionMethod($dataColumn, 'renderFilterCellContent'); + $method->setAccessible(true); + $result = $method->invoke($dataColumn); + + $this->assertEquals('', $result); + } } diff --git a/tests/framework/helpers/ArrayHelperTest.php b/tests/framework/helpers/ArrayHelperTest.php index 0a60eff..fae9d02 100644 --- a/tests/framework/helpers/ArrayHelperTest.php +++ b/tests/framework/helpers/ArrayHelperTest.php @@ -7,7 +7,10 @@ namespace yiiunit\framework\helpers; +use ArrayAccess; +use Iterator; use yii\base\BaseObject; +use yii\base\Model; use yii\data\Sort; use yii\helpers\ArrayHelper; use yiiunit\TestCase; @@ -41,6 +44,102 @@ class Post3 extends BaseObject } } +class ArrayAccessibleObject implements ArrayAccess +{ + public $name = 'bar1'; + protected $container = []; + + public function __construct($container) + { + $this->container = $container; + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + public function offsetExists($offset) + { + return array_key_exists($offset, $this->container); + } + + public function offsetUnset($offset) + { + unset($this->container[$offset]); + } + + public function offsetGet($offset) + { + return $this->offsetExists($offset) ? $this->container[$offset] : null; + } +} + +class TraversableArrayAccessibleObject extends ArrayAccessibleObject implements Iterator +{ + private $position = 0; + + public function __construct($container) + { + $this->position = 0; + + parent::__construct($container); + } + + protected function getContainerKey($keyIndex) + { + $keys = array_keys($this->container); + return array_key_exists($keyIndex, $keys) ? $keys[$keyIndex] : false; + } + + public function rewind() + { + $this->position = 0; + } + + public function current() + { + return $this->offsetGet($this->getContainerKey($this->position)); + } + + public function key() + { + return $this->getContainerKey($this->position); + } + + public function next() + { + ++$this->position; + } + + public function valid() + { + $key = $this->getContainerKey($this->position); + return !(!$key || !$this->offsetExists($key)); + } +} + +class MagicModel extends Model +{ + protected $magic; + + public function getMagic() + { + return 42; + } + + private $moreMagic; + + public function getMoreMagic() + { + return 'ta-da'; + } +} + /** * @group helpers */ @@ -413,6 +512,17 @@ class ArrayHelperTest extends TestCase $this->assertEquals($expected, $result); } + public function testMergeWithNumericKeys() + { + $a = [10 => [1]]; + $b = [10 => [2]]; + + $result = ArrayHelper::merge($a, $b); + + $expected = [10 => [1], 11 => [2]]; + $this->assertEquals($expected, $result); + } + public function testMergeWithUnset() { $a = [ @@ -734,6 +844,33 @@ class ArrayHelperTest extends TestCase $this->assertFalse(ArrayHelper::keyExists('c', $array, false)); } + public function testKeyExistsArrayAccess() + { + $array = new TraversableArrayAccessibleObject([ + 'a' => 1, + 'B' => 2, + ]); + + $this->assertTrue(ArrayHelper::keyExists('a', $array)); + $this->assertFalse(ArrayHelper::keyExists('b', $array)); + $this->assertTrue(ArrayHelper::keyExists('B', $array)); + $this->assertFalse(ArrayHelper::keyExists('c', $array)); + } + + /** + * @expectedException \yii\base\InvalidArgumentException + * @expectedExceptionMessage Second parameter($array) cannot be ArrayAccess in case insensitive mode + */ + public function testKeyExistsArrayAccessCaseInsensitiveThrowsError() + { + $array = new TraversableArrayAccessibleObject([ + 'a' => 1, + 'B' => 2, + ]); + + ArrayHelper::keyExists('a', $array, false); + } + public function valueProvider() { return [ @@ -830,6 +967,45 @@ class ArrayHelperTest extends TestCase $this->assertEquals(23, ArrayHelper::getValue($arrayObject, 'nonExisting')); } + public function testGetValueFromArrayAccess() + { + $arrayAccessibleObject = new ArrayAccessibleObject([ + 'one' => 1, + 'two' => 2, + 'three' => 3, + 'key.with.dot' => 'dot', + 'null' => null, + ]); + + $this->assertEquals(1, ArrayHelper::getValue($arrayAccessibleObject, 'one')); + } + + public function testGetValueWithDotsFromArrayAccess() + { + $arrayAccessibleObject = new ArrayAccessibleObject([ + 'one' => 1, + 'two' => 2, + 'three' => 3, + 'key.with.dot' => 'dot', + 'null' => null, + ]); + + $this->assertEquals('dot', ArrayHelper::getValue($arrayAccessibleObject, 'key.with.dot')); + } + + public function testGetValueNonexistingArrayAccess() + { + $arrayAccessibleObject = new ArrayAccessibleObject([ + 'one' => 1, + 'two' => 2, + 'three' => 3, + 'key.with.dot' => 'dot', + 'null' => null, + ]); + + $this->assertEquals(null, ArrayHelper::getValue($arrayAccessibleObject, 'four')); + } + /** * Data provider for [[testSetValue()]]. * @return array test data @@ -1235,41 +1411,107 @@ class ArrayHelperTest extends TestCase 'A' => [ 'B' => 1, 'C' => 2, + 'D' => [ + 'E' => 1, + 'F' => 2, + ], ], 'G' => 1, ]; - $this->assertEquals(ArrayHelper::filter($array, ['A']), [ + + //Include tests + $this->assertEquals([ 'A' => [ 'B' => 1, 'C' => 2, + 'D' => [ + 'E' => 1, + 'F' => 2, + ], ], - ]); - $this->assertEquals(ArrayHelper::filter($array, ['A.B']), [ + ], ArrayHelper::filter($array, ['A'])); + $this->assertEquals([ 'A' => [ 'B' => 1, ], - ]); - $this->assertEquals(ArrayHelper::filter($array, ['A', '!A.B']), [ + ], ArrayHelper::filter($array, ['A.B'])); + $this->assertEquals([ + 'A' => [ + 'D' => [ + 'E' => 1, + 'F' => 2, + ], + ], + ], ArrayHelper::filter($array, ['A.D'])); + $this->assertEquals([ + 'A' => [ + 'D' => [ + 'E' => 1, + ], + ], + ], ArrayHelper::filter($array, ['A.D.E'])); + $this->assertEquals([ 'A' => [ + 'B' => 1, 'C' => 2, + 'D' => [ + 'E' => 1, + 'F' => 2, + ], ], - ]); - $this->assertEquals(ArrayHelper::filter($array, ['!A.B', 'A']), [ + 'G' => 1, + ], ArrayHelper::filter($array, ['A', 'G'])); + $this->assertEquals([ + 'A' => [ + 'D' => [ + 'E' => 1, + ], + ], + 'G' => 1, + ], ArrayHelper::filter($array, ['A.D.E', 'G'])); + + //Exclude (combined with include) tests + $this->assertEquals([ 'A' => [ 'C' => 2, + 'D' => [ + 'E' => 1, + 'F' => 2, + ], ], - ]); - $this->assertEquals(ArrayHelper::filter($array, ['A', 'G']), [ + ], ArrayHelper::filter($array, ['A', '!A.B'])); + $this->assertEquals([ + 'A' => [ + 'C' => 2, + 'D' => [ + 'E' => 1, + 'F' => 2, + ], + ], + ], ArrayHelper::filter($array, ['!A.B', 'A'])); + $this->assertEquals([ 'A' => [ 'B' => 1, 'C' => 2, + 'D' => [ + 'F' => 2, + ], ], - 'G' => 1, - ]); - $this->assertEquals(ArrayHelper::filter($array, ['X']), []); - $this->assertEquals(ArrayHelper::filter($array, ['X.Y']), []); - $this->assertEquals(ArrayHelper::filter($array, ['A.X']), []); + ], ArrayHelper::filter($array, ['A', '!A.D.E'])); + $this->assertEquals([ + 'A' => [ + 'B' => 1, + 'C' => 2, + ], + ], ArrayHelper::filter($array, ['A', '!A.D'])); + //Non existing keys tests + $this->assertEquals([], ArrayHelper::filter($array, ['X'])); + $this->assertEquals([], ArrayHelper::filter($array, ['X.Y'])); + $this->assertEquals([], ArrayHelper::filter($array, ['X.Y.Z'])); + $this->assertEquals([], ArrayHelper::filter($array, ['A.X'])); + + //Values that evaluate to `true` with `empty()` tests $tmp = [ 'a' => 0, 'b' => '', @@ -1278,6 +1520,49 @@ class ArrayHelperTest extends TestCase 'e' => true, ]; - $this->assertEquals(ArrayHelper::filter($tmp, array_keys($tmp)), $tmp); + $this->assertEquals($tmp, ArrayHelper::filter($tmp, array_keys($tmp))); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18395 + */ + public function testFilterForIntegerKeys() + { + $array = ['a', 'b', ['c', 'd']]; + + // to make sure order is changed test it encoded + $this->assertEquals('{"1":"b","0":"a"}', json_encode(ArrayHelper::filter($array, [1, 0]))); + $this->assertEquals([2 => ['c']], ArrayHelper::filter($array, ['2.0'])); + $this->assertEquals([2 => [1 => 'd']], ArrayHelper::filter($array, [2, '!2.0'])); + } + + public function testFilterWithInvalidValues() + { + $array = ['a' => 'b']; + + $this->assertEquals([], ArrayHelper::filter($array, [new \stdClass()])); + $this->assertEquals([], ArrayHelper::filter($array, [['a']])); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18086 + */ + public function testArrayAccessWithPublicProperty() + { + $data = new ArrayAccessibleObject(['value' => 123]); + + $this->assertEquals(123, ArrayHelper::getValue($data, 'value')); + $this->assertEquals('bar1', ArrayHelper::getValue($data, 'name')); + } + + /** + * https://github.com/yiisoft/yii2/commit/35fb9c624893855317e5fe52e6a21f6518a9a31c changed the way + * ArrayHelper works with existing object properties in case of ArrayAccess. + */ + public function testArrayAccessWithMagicProperty() + { + $model = new MagicModel(); + $this->assertEquals(42, ArrayHelper::getValue($model, 'magic')); + $this->assertEquals('ta-da', ArrayHelper::getValue($model, 'moreMagic')); } } diff --git a/tests/framework/helpers/BaseConsoleTest.php b/tests/framework/helpers/BaseConsoleTest.php index 874c237..0a655df 100644 --- a/tests/framework/helpers/BaseConsoleTest.php +++ b/tests/framework/helpers/BaseConsoleTest.php @@ -12,6 +12,12 @@ use yii\helpers\BaseConsole; */ class BaseConsoleTest extends TestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + /** * @test */ @@ -21,9 +27,57 @@ class BaseConsoleTest extends TestCase $actual = BaseConsole::renderColoredString($data); $expected = "\033[33mfoo"; $this->assertEquals($expected, $actual); - + $actual = BaseConsole::renderColoredString($data, false); $expected = "foo"; $this->assertEquals($expected, $actual); } + + /** + * @test + */ + public function ansiColorizedSubstr_withoutColors() + { + $str = 'FooBar'; + + $actual = BaseConsole::ansiColorizedSubstr($str, 0, 3); + $expected = BaseConsole::renderColoredString('Foo'); + $this->assertEquals($expected, $actual); + + $actual = BaseConsole::ansiColorizedSubstr($str, 3, 3); + $expected = BaseConsole::renderColoredString('Bar'); + $this->assertEquals($expected, $actual); + + $actual = BaseConsole::ansiColorizedSubstr($str, 1, 4); + $expected = BaseConsole::renderColoredString('ooBa'); + $this->assertEquals($expected, $actual); + } + + /** + * @test + * @dataProvider ansiColorizedSubstr_withColors_data + * @param $str + * @param $start + * @param $length + * @param $expected + */ + public function ansiColorizedSubstr_withColors($str, $start, $length, $expected) + { + $ansiStr = BaseConsole::renderColoredString($str); + + $ansiActual = BaseConsole::ansiColorizedSubstr($ansiStr, $start, $length); + $ansiExpected = BaseConsole::renderColoredString($expected); + $this->assertEquals($ansiExpected, $ansiActual); + } + + public function ansiColorizedSubstr_withColors_data() + { + return [ + ['%rFoo%gBar%n', 0, 3, '%rFoo%n'], + ['%rFoo%gBar%n', 3, 3, '%gBar%n'], + ['%rFoo%gBar%n', 1, 4, '%roo%gBa%n'], + ['Foo%yBar%nYes', 1, 7, 'oo%yBar%nYe'], + ['Foo%yBar%nYes', 5, 3, '%yr%nYe'], + ]; + } } diff --git a/tests/framework/helpers/FileHelperTest.php b/tests/framework/helpers/FileHelperTest.php index 9e46f0d..7fd94a9 100644 --- a/tests/framework/helpers/FileHelperTest.php +++ b/tests/framework/helpers/FileHelperTest.php @@ -396,7 +396,7 @@ class FileHelperTest extends TestCase public function testRemoveDirectorySymlinks1() { - if (strtolower(substr(PHP_OS, 0, 3)) == 'win') { + if (DIRECTORY_SEPARATOR === '\\') { $this->markTestSkipped('Cannot test this on MS Windows since symlinks are uncommon for it.'); } @@ -439,7 +439,7 @@ class FileHelperTest extends TestCase public function testRemoveDirectorySymlinks2() { - if (strtolower(substr(PHP_OS, 0, 3)) == 'win') { + if (DIRECTORY_SEPARATOR === '\\') { $this->markTestSkipped('Cannot test this on MS Windows since symlinks are uncommon for it.'); } diff --git a/tests/framework/helpers/HtmlTest.php b/tests/framework/helpers/HtmlTest.php index 950a8c3..28998e6 100644 --- a/tests/framework/helpers/HtmlTest.php +++ b/tests/framework/helpers/HtmlTest.php @@ -9,6 +9,7 @@ namespace yiiunit\framework\helpers; use Yii; use yii\base\DynamicModel; +use yii\db\ArrayExpression; use yii\helpers\Html; use yii\helpers\Url; use yiiunit\TestCase; @@ -767,6 +768,63 @@ EOD; 'label' => 'Test Label' ] ])); + + $expected = <<<'EOD' +
+ +
+EOD; + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['1.1'], ['1' => '1', '1.1' => '1.1', '1.10' => '1.10'], ['strict' => true])); + } + + public function testRadioListWithArrayExpression() + { + $selection = new ArrayExpression(['first']); + + $output = Html::radioList( + 'test', + $selection, + [ + 'first' => 'first', + 'second' => 'second' + ] + ); + + $this->assertEqualsWithoutLE('
+
', $output); + } + + public function testCheckboxListWithArrayExpression() + { + $selection = new ArrayExpression(['first']); + + $output = Html::checkboxList( + 'test', + $selection, + [ + 'first' => 'first', + 'second' => 'second' + ] + ); + + $this->assertEqualsWithoutLE('
+
', $output); + } + + public function testRenderSelectOptionsWithArrayExpression() + { + $selection = new ArrayExpression(['first']); + + $output = Html::renderSelectOptions( + $selection, + [ + 'first' => 'first', + 'second' => 'second' + ] + ); + + $this->assertEqualsWithoutLE(' +', $output); } public function testRadioList() @@ -858,6 +916,13 @@ EOD; 'label' => 'Test Label' ] ])); + + $expected = <<<'EOD' +
+ +
+EOD; + $this->assertEqualsWithoutLE($expected, Html::radioList('test', ['1.1'], ['1' => '1', '1.1' => '1.1', '1.10' => '1.10'], ['strict' => true])); } public function testUl() @@ -995,6 +1060,26 @@ EOD; ], ]; $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(['value1'], $data, $attributes)); + + $expected = <<<'EOD' + + + +EOD; + $data = ['1' => '1', '1.1' => '1.1', '1.10' => '1.10']; + $attributes = ['strict' => true]; + $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(['1.1'], $data, $attributes)); + + $expected = <<<'EOD' + + + + + +EOD; + $data = ['1' => '1', '1.1' => '1.1', 'group' => ['1.10' => '1.10']]; + $attributes = ['strict' => true]; + $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(['1.10'], $data, $attributes)); } public function testRenderAttributes() @@ -1013,6 +1098,29 @@ EOD; ], ]; $this->assertEquals(' data-foo=\'[]\'', Html::renderTagAttributes($attributes)); + + $attributes = [ + 'data' => [ + 'foo' => true, + ], + ]; + $this->assertEquals(' data-foo', Html::renderTagAttributes($attributes)); + + + $attributes = [ + 'data' => [ + 'foo' => false, + ], + ]; + $this->assertEquals('', Html::renderTagAttributes($attributes)); + + + $attributes = [ + 'data' => [ + 'foo' => null, + ], + ]; + $this->assertEquals('', Html::renderTagAttributes($attributes)); } public function testAddCssClass() @@ -1173,6 +1281,7 @@ EOD; public function testDataAttributes() { + $this->assertEquals('', Html::tag('link', '', ['src' => 'xyz', 'aria' => ['a' => 1, 'b' => 'c']])); $this->assertEquals('', Html::tag('link', '', ['src' => 'xyz', 'data' => ['a' => 1, 'b' => 'c']])); $this->assertEquals('', Html::tag('link', '', ['src' => 'xyz', 'ng' => ['a' => 1, 'b' => 'c']])); $this->assertEquals('', Html::tag('link', '', ['src' => 'xyz', 'data-ng' => ['a' => 1, 'b' => 'c']])); @@ -1409,7 +1518,7 @@ EOD; } $model->validate(null, false); - $this->assertEquals($expectedHtml, Html::errorSummary($model, $options)); + $this->assertEqualsWithoutLE($expectedHtml, Html::errorSummary($model, $options)); } public function testError() @@ -1625,23 +1734,16 @@ EOD; ['a[0]', 'a'], ['[0]a[0]', 'a'], ['[0]a.[0]', 'a.'], + ['ä', 'ä'], + ['ä', 'ä'], + ['asdf]öáöio..[asdfasdf', 'öáöio..'], + ['öáöio', 'öáöio'], + ['[0]test.ööößß.d', 'test.ööößß.d'], + ['ИІК', 'ИІК'], + [']ИІК[', 'ИІК'], + ['[0]ИІК[0]', 'ИІК'], ]; - if (getenv('TRAVIS_PHP_VERSION') !== 'nightly') { - $data = array_merge($data, [ - ['ä', 'ä'], - ['ä', 'ä'], - ['asdf]öáöio..[asdfasdf', 'öáöio..'], - ['öáöio', 'öáöio'], - ['[0]test.ööößß.d', 'test.ööößß.d'], - ['ИІК', 'ИІК'], - [']ИІК[', 'ИІК'], - ['[0]ИІК[0]', 'ИІК'], - ]); - } else { - $this->markTestIncomplete("Unicode characters check skipped for 'nightly' PHP version because \w does not work with these as expected. Check later with stable version."); - } - return $data; } @@ -1819,7 +1921,7 @@ HTML; $this->assertEqualsWithoutLE($expected, $actual); } - public function testActiveCheckboxList() + public function testActiveRadioList() { $model = new HtmlTestModel(); @@ -1830,7 +1932,7 @@ HTML; $this->assertEqualsWithoutLE($expected, $actual); } - public function testActiveRadioList() + public function testActiveCheckboxList() { $model = new HtmlTestModel(); @@ -1841,6 +1943,17 @@ HTML; $this->assertEqualsWithoutLE($expected, $actual); } + public function testActiveCheckboxList_options() + { + $model = new HtmlTestModel(); + + $expected = <<<'HTML' +
+HTML; + $actual = Html::activeCheckboxList($model, 'types', ['foo'], ['name' => 'foo', 'value' => 0]); + $this->assertEqualsWithoutLE($expected, $actual); + } + public function testActiveTextInput_placeholderFillFromModel() { $model = new HtmlTestModel(); diff --git a/tests/framework/helpers/InflectorTest.php b/tests/framework/helpers/InflectorTest.php index 3d33912..7b80522 100644 --- a/tests/framework/helpers/InflectorTest.php +++ b/tests/framework/helpers/InflectorTest.php @@ -194,8 +194,10 @@ class InflectorTest extends TestCase { $this->assertEquals('dont_replace_replacement', Inflector::slug('dont replace_replacement', '_')); $this->assertEquals('remove_trailing_replacements', Inflector::slug('_remove trailing replacements_', '_')); + $this->assertEquals('remove_excess_replacements', Inflector::slug(' _ _ remove excess _ _ replacements_', '_')); $this->assertEquals('thisrepisreprepreplacement', Inflector::slug('this is REP-lacement', 'REP')); $this->assertEquals('0_100_kmh', Inflector::slug('0-100 Km/h', '_')); + $this->assertEquals('testtext', Inflector::slug('test text', '')); } public function testSlugIntl() diff --git a/tests/framework/helpers/JsonTest.php b/tests/framework/helpers/JsonTest.php index 3952788..785b093 100644 --- a/tests/framework/helpers/JsonTest.php +++ b/tests/framework/helpers/JsonTest.php @@ -220,6 +220,16 @@ class JsonTest extends TestCase $expectedHtml = '["Error message. Here are some chars: < >","Error message. Here are even more chars: \"\""]'; $this->assertEquals($expectedHtml, Json::errorSummary($model, $options)); } + + /** + * @link https://github.com/yiisoft/yii2/issues/17760 + */ + public function testEncodeDateTime() + { + $input = new \DateTime('October 12, 2014', new \DateTimeZone('UTC')); + $output = Json::encode($input); + $this->assertEquals('{"date":"2014-10-12 00:00:00.000000","timezone_type":3,"timezone":"UTC"}', $output); + } } class JsonModel extends DynamicModel implements \JsonSerializable diff --git a/tests/framework/i18n/FormatterDateTest.php b/tests/framework/i18n/FormatterDateTest.php index dd1427b..ceb1aa0 100644 --- a/tests/framework/i18n/FormatterDateTest.php +++ b/tests/framework/i18n/FormatterDateTest.php @@ -192,22 +192,24 @@ class FormatterDateTest extends TestCase $this->assertRegExp(date('~M j, Y,? g:i:s A~', $value->getTimestamp()), $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 (version_compare(PHP_VERSION, '5.5.0', '>=')) { + 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->assertSame(date('Y/m/d h:i:s A', $value->getTimestamp()), $this->formatter->asDatetime($value, 'php:Y/m/d h:i:s A')); } + if (PHP_VERSION_ID >= 50600) { + // DATE_ATOM + $value = time(); + $this->assertEquals(date(DATE_ATOM, $value), $this->formatter->asDatetime($value, 'php:' . DATE_ATOM)); + } + // 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)); // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null)); - - // DATE_ATOM - $value = time(); - $this->assertEquals(date(DATE_ATOM, $value), $this->formatter->asDatetime($value, 'php:' . DATE_ATOM)); } public function testIntlAsTimestamp() diff --git a/tests/framework/i18n/FormatterNumberTest.php b/tests/framework/i18n/FormatterNumberTest.php index 3609bd4..6d1b7b7 100755 --- a/tests/framework/i18n/FormatterNumberTest.php +++ b/tests/framework/i18n/FormatterNumberTest.php @@ -412,6 +412,14 @@ class FormatterNumberTest extends TestCase $this->formatter->thousandSeparator = ' '; $this->formatter->decimalSeparator = ','; $this->assertSame('USD 95 836 208 451 783 051,86', $this->formatter->asCurrency('95836208451783051.864', 'USD')); + + // different currency decimal separator + $this->formatter->locale = 'ru-RU'; + $this->assertIsOneOf($this->formatter->asCurrency('123'), ["123,00\xc2\xa0₽", "123,00\xc2\xa0руб."]); + $this->formatter->currencyDecimalSeparator = ','; + $this->assertIsOneOf($this->formatter->asCurrency('123'), ["123,00\xc2\xa0₽", "123,00\xc2\xa0руб."]); + $this->formatter->currencyDecimalSeparator = '.'; + $this->assertIsOneOf($this->formatter->asCurrency('123'), ["123.00\xc2\xa0₽", "123.00\xc2\xa0руб."]); } /** @@ -514,12 +522,12 @@ class FormatterNumberTest extends TestCase public function testIntlAsScientific() { - $value = '123'; - $this->assertSame('1.23E2', $this->formatter->asScientific($value)); - $value = '123456'; - $this->assertSame('1.23456E5', $this->formatter->asScientific($value)); - $value = '-123456.123'; - $this->assertSame('-1.23456123E5', $this->formatter->asScientific($value)); + // see https://github.com/yiisoft/yii2/issues/17708 + $this->markTestSkipped('The test is unreliable since output depends on ICU version'); + + $this->assertSame('1.23E2', $this->formatter->asScientific('123')); + $this->assertSame('1.23456E5', $this->formatter->asScientific('123456')); + $this->assertSame('-1.23456123E5', $this->formatter->asScientific('-123456.123')); // empty input $this->assertSame('0E0', $this->formatter->asScientific(false)); @@ -533,12 +541,9 @@ class FormatterNumberTest extends TestCase public function testAsScientific() { - $value = '123'; - $this->assertSame('1.23E+2', $this->formatter->asScientific($value, 2)); - $value = '123456'; - $this->assertSame('1.234560E+5', $this->formatter->asScientific($value)); - $value = '-123456.123'; - $this->assertSame('-1.234561E+5', $this->formatter->asScientific($value)); + $this->assertSame('1.23E+2', $this->formatter->asScientific('123', 2)); + $this->assertSame('1.234560E+5', $this->formatter->asScientific('123456')); + $this->assertSame('-1.234561E+5', $this->formatter->asScientific('-123456.123')); // empty input $this->assertSame('0.000000E+0', $this->formatter->asScientific(false)); diff --git a/tests/framework/i18n/FormatterTest.php b/tests/framework/i18n/FormatterTest.php index 1621d3a..4716c67 100644 --- a/tests/framework/i18n/FormatterTest.php +++ b/tests/framework/i18n/FormatterTest.php @@ -75,6 +75,20 @@ class FormatterTest extends TestCase $this->assertEquals('ru-RU', $f->locale); } + public function testLanguage() + { + // language is configured explicitly + $f = new Formatter(['language' => 'en-US']); + $this->assertEquals('en-US', $f->language); + + // language is configured via locale (omitting @calendar param) + $f = new Formatter(['locale' => 'en-US@calendar=persian']); + $this->assertEquals('en-US', $f->language); + + // if not, take from application + $f = new Formatter(); + $this->assertEquals('ru-RU', $f->language); + } public function testAsRaw() { diff --git a/tests/framework/i18n/I18NTest.php b/tests/framework/i18n/I18NTest.php index 2b28143..727dcbb 100644 --- a/tests/framework/i18n/I18NTest.php +++ b/tests/framework/i18n/I18NTest.php @@ -129,6 +129,9 @@ class I18NTest extends TestCase // target is a different language than source $this->assertEquals('Собака бегает быстро.', $i18n->translate('test', $msg, [], 'ru-RU')); $this->assertEquals('Собака бегает быстро.', $i18n->translate('test', $msg, [], 'ru')); + + // language is set to null + $this->assertEquals($msg, $i18n->translate('test', $msg, [], null)); } public function testTranslateParams() diff --git a/tests/framework/log/DispatcherTest.php b/tests/framework/log/DispatcherTest.php index ef14018..0f74cf6 100644 --- a/tests/framework/log/DispatcherTest.php +++ b/tests/framework/log/DispatcherTest.php @@ -18,6 +18,7 @@ namespace yii\log { namespace yiiunit\framework\log { + use yiiunit\framework\log\mocks\TargetMock; use Yii; use yii\base\UserException; use yii\log\Dispatcher; @@ -282,5 +283,39 @@ namespace yiiunit\framework\log { } static::fail("Function '$name' has not implemented yet!"); } + + private $targetThrowFirstCount; + private $targetThrowSecondOutputs; + + public function testTargetThrow() + { + $this->targetThrowFirstCount = 0; + $this->targetThrowSecondOutputs = []; + $targetFirst = new TargetMock([ + 'collectOverride' => function () { + $this->targetThrowFirstCount++; + if (PHP_MAJOR_VERSION < 7) { + throw new \RuntimeException('test'); + } + require_once __DIR__ . DIRECTORY_SEPARATOR . 'mocks' . DIRECTORY_SEPARATOR . 'typed_error.php'; + typed_error_test_mock([]); + } + ]); + $targetSecond = new TargetMock([ + 'collectOverride' => function ($message, $final) { + $this->targetThrowSecondOutputs[] = array_pop($message); + } + ]); + $dispatcher = new Dispatcher([ + 'logger' => new Logger(), + 'targets' => [$targetFirst, $targetSecond], + ]); + $message = 'test' . time(); + $dispatcher->dispatch([$message], false); + $this->assertSame(1, $this->targetThrowFirstCount); + $this->assertSame(2, count($this->targetThrowSecondOutputs)); + $this->assertSame($message, array_shift($this->targetThrowSecondOutputs)); + $this->assertStringStartsWith('Unable to send log via', array_shift($this->targetThrowSecondOutputs)[0]); + } } } diff --git a/tests/framework/log/mocks/TargetMock.php b/tests/framework/log/mocks/TargetMock.php new file mode 100644 index 0000000..c2189f9 --- /dev/null +++ b/tests/framework/log/mocks/TargetMock.php @@ -0,0 +1,28 @@ +collectOverride !== null) { + call_user_func($this->collectOverride, $messages, $final); + return; + } + parent::collect($messages, $final); // TODO: Change the autogenerated stub + } +} diff --git a/tests/framework/log/mocks/typed_error.php b/tests/framework/log/mocks/typed_error.php new file mode 100644 index 0000000..fba864d --- /dev/null +++ b/tests/framework/log/mocks/typed_error.php @@ -0,0 +1,4 @@ +assertFalse($mutexTwo->release($mutexName)); } + /** + * @dataProvider mutexDataProvider() + * + * @param string $mutexName + */ + public function testMutexIsAcquired($mutexName) + { + $mutexOne = $this->createMutex(); + $mutexTwo = $this->createMutex(); + + $this->assertFalse($mutexOne->isAcquired($mutexName)); + $this->assertTrue($mutexOne->acquire($mutexName)); + $this->assertTrue($mutexOne->isAcquired($mutexName)); + + $this->assertFalse($mutexTwo->isAcquired($mutexName)); + + $this->assertTrue($mutexOne->release($mutexName)); + $this->assertFalse($mutexOne->isAcquired($mutexName)); + + $this->assertFalse($mutexOne->isAcquired('non existing')); + } + public static function mutexDataProvider() { $utf = <<<'UTF' diff --git a/tests/framework/mutex/RetryAcquireTraitTest.php b/tests/framework/mutex/RetryAcquireTraitTest.php index 8d5711f..5b80bfb 100644 --- a/tests/framework/mutex/RetryAcquireTraitTest.php +++ b/tests/framework/mutex/RetryAcquireTraitTest.php @@ -30,30 +30,67 @@ class RetryAcquireTraitTest extends TestCase $mutexOne = $this->createMutex(); $mutexTwo = $this->createMutex(); - $this->assertTrue($mutexOne->acquire($mutexName)); - $this->assertFalse($mutexTwo->acquire($mutexName, 1)); + $this->assertTrue( + $mutexOne->acquire($mutexName), + 'Failed to acquire first mutex.' + ); + $this->assertFalse( + $mutexTwo->acquire($mutexName, 1), + 'Second mutex was acquired but should have timed out.' + ); - $this->assertGreaterThanOrEqual(1, count($mutexTwo->attemptsTime)); - $this->assertLessThanOrEqual(20, count($mutexTwo->attemptsTime)); + $this->assertGreaterThanOrEqual( + 1, + count($mutexTwo->attemptsTime), + 'There should be at least one atttempt to acquire first mutex.' + ); + $this->assertLessThanOrEqual( + 20, + count($mutexTwo->attemptsTime), + 'There could be no more than 20 attempts consideing 50ms delay and 1s timeout.' + ); - foreach ($mutexTwo->attemptsTime as $i => $attemptTime) { - if ($i === 0) { - continue; - } + // https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleep + // If dwMilliseconds is less than the resolution of the system clock, the thread may sleep for less + // than the specified length of time. + if (!$this->isRunningOnWindows()) { + foreach ($mutexTwo->attemptsTime as $i => $attemptTime) { + if ($i === 0) { + continue; + } - $intervalMilliseconds = ($mutexTwo->attemptsTime[$i] - $mutexTwo->attemptsTime[$i-1]) * 1000; - $this->assertGreaterThanOrEqual($mutexTwo->retryDelay, $intervalMilliseconds); + $attemptInterval = ($mutexTwo->attemptsTime[$i] - $mutexTwo->attemptsTime[$i - 1]) * 1000; + $this->assertGreaterThanOrEqual( + $mutexTwo->retryDelay, + $attemptInterval, + sprintf( + 'Retry delay of %s ms was not properly taken into account. Actual interval was %s ms.', + $mutexTwo->retryDelay, + $attemptInterval + ) + ); + } } } /** + * @return bool + */ + private function isRunningOnWindows() + { + return DIRECTORY_SEPARATOR === '\\'; + } + + /** * @return DumbMutex * @throws InvalidConfigException */ private function createMutex() { - return Yii::createObject([ - 'class' => DumbMutex::className(), - ]); + return Yii::createObject( + [ + 'class' => DumbMutex::className(), + ] + ); } } diff --git a/tests/framework/rest/SerializerTest.php b/tests/framework/rest/SerializerTest.php index 1bfe780..6fd7097 100644 --- a/tests/framework/rest/SerializerTest.php +++ b/tests/framework/rest/SerializerTest.php @@ -414,6 +414,65 @@ class SerializerTest extends TestCase $this->assertEquals($expectedResult, $serializer->serialize($dataProvider)); } + + /** + * @see https://github.com/yiisoft/yii2/issues/16334 + */ + public function testSerializeJsonSerializable() + { + $serializer = new Serializer(); + $model3 = new TestModel3(); + $model4 = new TestModel4(); + + $this->assertEquals(['customField' => 'test3/test4'], $serializer->serialize($model3)); + $this->assertEquals(['customField2' => 'test5/test6'], $serializer->serialize($model4)); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/16334 + */ + public function testSerializeArrayableWithJsonSerializableAttribute() + { + $serializer = new Serializer(); + $model = new TestModel5(); + + $this->assertEquals( + [ + 'field7' => 'test7', + 'field8' => 'test8', + 'testModel3' => ['customField' => 'test3/test4'], + 'testModel4' => ['customField2' => 'test5/test6'], + 'testModelArray' => [['customField' => 'test3/test4'], ['customField2' => 'test5/test6']], + ], + $serializer->serialize($model) + ); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/17886 + */ + public function testSerializeArray() + { + $serializer = new Serializer(); + $model1 = new TestModel(); + $model2 = new TestModel(); + $model3 = new TestModel(); + + $this->assertSame([ + [ + 'field1' => 'test', + 'field2' => 2, + ], + [ + 'field1' => 'test', + 'field2' => 2, + ], + 'testKey' => [ + 'field1' => 'test', + 'field2' => 2, + ], + ], $serializer->serialize([$model1, $model2, 'testKey' => $model3])); + } } class TestModel extends Model @@ -457,3 +516,84 @@ class TestModel2 extends Model return static::$extraFields; } } + +class TestModel3 extends Model implements \JsonSerializable +{ + public static $fields = ['field3', 'field4']; + public static $extraFields = []; + + public $field3 = 'test3'; + public $field4 = 'test4'; + public $extraField4 = 'testExtra2'; + + public function fields() + { + return [ + 'customField' => function() { + return $this->field3.'/'.$this->field4; + }, + ]; + } + + public function extraFields() + { + return static::$extraFields; + } + + public function jsonSerialize() + { + return $this->getAttributes(); + } +} +class TestModel4 implements \JsonSerializable +{ + public $field5 = 'test5'; + public $field6 = 'test6'; + + public function jsonSerialize() + { + return [ + 'customField2' => $this->field5.'/'.$this->field6, + ]; + } +} + +class TestModel5 extends Model +{ + public static $fields = ['field7', 'field8']; + public static $extraFields = []; + + public $field7 = 'test7'; + public $field8 = 'test8'; + public $extraField4 = 'testExtra4'; + + public function fields() + { + $fields = static::$fields; + $fields['testModel3'] = function() { + return $this->getTestModel3(); + }; + $fields['testModel4'] = function() { + return $this->getTestModel4(); + }; + $fields['testModelArray'] = function() { + return [$this->getTestModel3(), $this->getTestModel4()]; + }; + return $fields; + } + + public function extraFields() + { + return static::$extraFields; + } + + public function getTestModel3() + { + return new TestModel3(); + } + + public function getTestModel4() + { + return new TestModel4(); + } +} diff --git a/tests/framework/rest/UrlRuleTest.php b/tests/framework/rest/UrlRuleTest.php index da738c2..e52eb79 100644 --- a/tests/framework/rest/UrlRuleTest.php +++ b/tests/framework/rest/UrlRuleTest.php @@ -86,6 +86,8 @@ class UrlRuleTest extends TestCase ['controller' => 'post', 'prefix' => 'admin'], [ ['admin/posts', 'post/index'], + ['different/posts', false], + ['posts', false], ], ], [ @@ -323,7 +325,7 @@ class UrlRuleTest extends TestCase 'extraPatterns' => [ '{id}/my' => 'my', 'my' => 'my', - // this should not create a URL, no GET definition + // since 2.0.41 this should create a URL (previously it was false) 'POST {id}/my2' => 'my2', ], ], @@ -339,7 +341,7 @@ class UrlRuleTest extends TestCase [['v1/channel/my'], 'v1/channels/my'], [['v1/channel/my', 'id' => 42], 'v1/channels/42/my'], [['v1/channel/my2'], false], - [['v1/channel/my2', 'id' => 42], false], + [['v1/channel/my2', 'id' => 42], 'v1/channels/42/my2'], ], ], ]; @@ -419,11 +421,16 @@ class UrlRuleTest extends TestCase [['v1/channel/index'], 'v1/channels', WebUrlRule::CREATE_STATUS_SUCCESS], [['v1/channel/index', 'offset' => 1], 'v1/channels?offset=1', WebUrlRule::CREATE_STATUS_SUCCESS], [['v1/channel/view', 'id' => 42], 'v1/channels/42', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/channel/view'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], [['v1/channel/options'], 'v1/channels', WebUrlRule::CREATE_STATUS_SUCCESS], [['v1/channel/options', 'id' => 42], 'v1/channels/42', WebUrlRule::CREATE_STATUS_SUCCESS], - [['v1/channel/delete'], false, WebUrlRule::CREATE_STATUS_PARSING_ONLY], + [['v1/channel/delete'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], + [['v1/channel/delete', 'id' => 43], 'v1/channels/43', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/channel/create'], 'v1/channels', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/channel/update', 'id' => 44], 'v1/channels/44', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/channel/update'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], + [['v1/missing/view'], false, WebUrlRule::CREATE_STATUS_ROUTE_MISMATCH], - [['v1/channel/view'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], ], ], 'multiple controllers' => [ @@ -438,13 +445,23 @@ class UrlRuleTest extends TestCase [['v1/channel/view', 'id' => 42], 'v1/channel/42', WebUrlRule::CREATE_STATUS_SUCCESS], [['v1/channel/options'], 'v1/channel', WebUrlRule::CREATE_STATUS_SUCCESS], [['v1/channel/options', 'id' => 42], 'v1/channel/42', WebUrlRule::CREATE_STATUS_SUCCESS], - [['v1/channel/delete'], false, WebUrlRule::CREATE_STATUS_PARSING_ONLY], + [['v1/channel/delete'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], + [['v1/channel/delete', 'id' => 43], 'v1/channel/43', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/channel/create'], 'v1/channel', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/channel/update'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], + [['v1/channel/update', 'id' => 45], 'v1/channel/45', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/user/index'], 'v1/u', WebUrlRule::CREATE_STATUS_SUCCESS], [['v1/user/view', 'id' => 1], 'v1/u/1', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/user/view'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], [['v1/user/options'], 'v1/u', WebUrlRule::CREATE_STATUS_SUCCESS], [['v1/user/options', 'id' => 42], 'v1/u/42', WebUrlRule::CREATE_STATUS_SUCCESS], - [['v1/user/delete'], false, WebUrlRule::CREATE_STATUS_PARSING_ONLY], - [['v1/user/view'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], + [['v1/user/delete', 'id' => 44], 'v1/u/44', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/user/delete'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], + [['v1/user/create'], 'v1/u', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/user/update', 'id' => 46], 'v1/u/46', WebUrlRule::CREATE_STATUS_SUCCESS], + [['v1/user/update'], false, WebUrlRule::CREATE_STATUS_PARAMS_MISMATCH], + [['v1/missing/view'], false, WebUrlRule::CREATE_STATUS_ROUTE_MISMATCH], ], ], diff --git a/tests/framework/test/FixtureTest.php b/tests/framework/test/FixtureTest.php index 9e06ac8..e5b6657 100644 --- a/tests/framework/test/FixtureTest.php +++ b/tests/framework/test/FixtureTest.php @@ -11,9 +11,6 @@ use yii\test\Fixture; use yii\test\FixtureTrait; use yiiunit\TestCase; -/** - * @group fixture - */ class Fixture1 extends Fixture { public $depends = ['yiiunit\framework\test\Fixture2']; @@ -56,6 +53,35 @@ class Fixture3 extends Fixture } } +class Fixture4 extends Fixture +{ + public $depends = ['yiiunit\framework\test\Fixture5']; + public function load() + { + MyTestCase::$load .= '4'; + } + + public function unload() + { + MyTestCase::$unload .= '4'; + } +} + +class Fixture5 extends Fixture +{ + public $depends = ['yiiunit\framework\test\Fixture4']; + public function load() + { + MyTestCase::$load .= '5'; + } + + public function unload() + { + MyTestCase::$unload .= '5'; + } +} + + class MyTestCase { use FixtureTrait; @@ -104,16 +130,30 @@ class MyTestCase 'fixture1' => Fixture1::className(), 'fixture3' => Fixture3::className(), ]; - case 7: - default: return [ + case 7: return [ 'fixture1' => Fixture1::className(), 'fixture2' => Fixture2::className(), 'fixture3' => Fixture3::className(), ]; + case 8: return [ + 'fixture4' => Fixture4::className(), + ]; + case 9: return [ + 'fixture5' => Fixture5::className(), + 'fixture4' => Fixture4::className(), + ]; + case 10: return [ + 'fixture3a' => Fixture3::className(), // duplicate fixtures may occur two fixtures depend on the same fixture. + 'fixture3b' => Fixture3::className(), + ]; + default: return []; } } } +/** + * @group fixture + */ class FixtureTest extends TestCase { public function testDependencies() @@ -145,14 +185,16 @@ class FixtureTest extends TestCase protected function getDependencyTests() { return [ - 0 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => false], - 1 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => false], - 2 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => false], - 3 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => true], - 4 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => false], - 5 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => true], - 6 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => true], - 7 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => true], + 0 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => false, 'fixture4' => false, 'fixture5' => false], + 1 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => false, 'fixture4' => false, 'fixture5' => false], + 2 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => false, 'fixture4' => false, 'fixture5' => false], + 3 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => true, 'fixture4' => false, 'fixture5' => false], + 4 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => false, 'fixture4' => false, 'fixture5' => false], + 5 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => true, 'fixture4' => false, 'fixture5' => false], + 6 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => true, 'fixture4' => false, 'fixture5' => false], + 7 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => true, 'fixture4' => false, 'fixture5' => false], + 8 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => false, 'fixture4' => true, 'fixture5' => false], + 9 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => false, 'fixture4' => true, 'fixture5' => true], ]; } @@ -167,6 +209,9 @@ class FixtureTest extends TestCase 5 => ['32', '23'], 6 => ['321', '123'], 7 => ['321', '123'], + 8 => ['54', '45'], + 9 => ['45', '54'], + 10 => ['3', '3'], ]; } } diff --git a/tests/framework/validators/CompareValidatorTest.php b/tests/framework/validators/CompareValidatorTest.php index c65cc0a..a0740de 100644 --- a/tests/framework/validators/CompareValidatorTest.php +++ b/tests/framework/validators/CompareValidatorTest.php @@ -40,6 +40,15 @@ class CompareValidatorTest extends TestCase $this->assertTrue($val->validate($value)); $this->assertTrue($val->validate((string) $value)); $this->assertFalse($val->validate($value + 1)); + + // Using a closure for compareValue + $val = new CompareValidator(['compareValue' => function() use ($value) { + return $value; + }]); + $this->assertTrue($val->validate($value)); + $this->assertTrue($val->validate((string) $value)); + $this->assertFalse($val->validate($value + 1)); + foreach ($this->getOperationTestData($value) as $op => $tests) { $val = new CompareValidator(['compareValue' => $value]); $val->operator = $op; diff --git a/tests/framework/validators/DateValidatorTest.php b/tests/framework/validators/DateValidatorTest.php index 91b627b..804c63a 100644 --- a/tests/framework/validators/DateValidatorTest.php +++ b/tests/framework/validators/DateValidatorTest.php @@ -361,7 +361,11 @@ class DateValidatorTest extends TestCase { date_default_timezone_set($timezone); - $val = new DateValidator(['format' => 'yyyy-MM-dd HH:mm:ss', 'timestampAttribute' => 'attr_timestamp', 'timeZone' => 'UTC']); + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd HH:mm:ss', + 'timestampAttribute' => 'attr_timestamp', + 'timeZone' => 'UTC' + ]); $model = new FakedValidationModel(); $model->attr_date = '2013-09-13 14:23:15'; $model->attr_timestamp = true; @@ -370,7 +374,11 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame(1379082195, $model->attr_timestamp); - $val = new DateValidator(['format' => 'yyyy-MM-dd HH:mm:ss', 'timestampAttribute' => 'attr_timestamp', 'timeZone' => 'Europe/Berlin']); + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd HH:mm:ss', + 'timestampAttribute' => 'attr_timestamp', + 'timeZone' => 'Europe/Berlin', + ]); $model = new FakedValidationModel(); $model->attr_date = '2013-09-13 16:23:15'; $model->attr_timestamp = true; @@ -379,7 +387,12 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame(1379082195, $model->attr_timestamp); - $val = new DateValidator(['format' => 'yyyy-MM-dd HH:mm:ss', 'timestampAttribute' => 'attr_timestamp', 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', 'timeZone' => 'UTC']); + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd HH:mm:ss', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'timeZone' => 'UTC', + ]); $model = new FakedValidationModel(); $model->attr_date = '2013-09-13 14:23:15'; $model->attr_timestamp = true; @@ -388,7 +401,12 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame('2013-09-13 14:23:15', $model->attr_timestamp); - $val = new DateValidator(['format' => 'yyyy-MM-dd HH:mm:ss', 'timestampAttribute' => 'attr_timestamp', 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', 'timeZone' => 'Europe/Berlin']); + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd HH:mm:ss', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'timeZone' => 'Europe/Berlin', + ]); $model = new FakedValidationModel(); $model->attr_date = '2013-09-13 16:23:15'; $model->attr_timestamp = true; @@ -397,7 +415,12 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame('2013-09-13 14:23:15', $model->attr_timestamp); - $val = new DateValidator(['format' => 'yyyy-MM-dd HH:mm:ss', 'timestampAttribute' => 'attr_timestamp', 'timestampAttributeFormat' => 'php:Y-m-d H:i:s', 'timeZone' => 'UTC']); + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd HH:mm:ss', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'php:Y-m-d H:i:s', + 'timeZone' => 'UTC', + ]); $model = new FakedValidationModel(); $model->attr_date = '2013-09-13 14:23:15'; $model->attr_timestamp = true; @@ -406,7 +429,12 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame('2013-09-13 14:23:15', $model->attr_timestamp); - $val = new DateValidator(['format' => 'yyyy-MM-dd HH:mm:ss', 'timestampAttribute' => 'attr_timestamp', 'timestampAttributeFormat' => 'php:Y-m-d H:i:s', 'timeZone' => 'Europe/Berlin']); + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd HH:mm:ss', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'php:Y-m-d H:i:s', + 'timeZone' => 'Europe/Berlin', + ]); $model = new FakedValidationModel(); $model->attr_date = '2013-09-13 16:23:15'; $model->attr_timestamp = true; @@ -414,6 +442,21 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_date')); $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame('2013-09-13 14:23:15', $model->attr_timestamp); + + // setting non-UTC defaultTimeZone should not impact values with format where time part is provided + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd HH:mm:ss', + 'timestampAttribute' => 'attr_timestamp', + 'timeZone' => 'UTC', + 'defaultTimeZone' => 'Europe/Berlin', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2013-09-13 16:23:15'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame(1379089395, $model->attr_timestamp); // = 2013-09-13 16:23:15 UTC } /** @@ -510,6 +553,7 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_date')); $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame('2017-06-15 00:00:00', $model->attr_timestamp); + $val = new DateValidator([ 'format' => 'php:Y-m-d', 'timestampAttribute' => 'attr_timestamp', @@ -538,6 +582,7 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_date')); $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame('2017-06-15 00:00:00', $model->attr_timestamp); + $val = new DateValidator([ 'format' => 'php:Y-m-d', 'timestampAttribute' => 'attr_timestamp', @@ -567,6 +612,7 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_date')); $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame('2017-06-15 02:00:00', $model->attr_timestamp); + $val = new DateValidator([ 'format' => 'php:Y-m-d', 'timestampAttribute' => 'attr_timestamp', @@ -597,6 +643,7 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_date')); $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame('2017-06-15 02:00:00', $model->attr_timestamp); + $val = new DateValidator([ 'format' => 'php:Y-m-d', 'timestampAttribute' => 'attr_timestamp', @@ -611,6 +658,225 @@ class DateValidatorTest extends TestCase $this->assertFalse($model->hasErrors('attr_date')); $this->assertFalse($model->hasErrors('attr_timestamp')); $this->assertSame('2017-06-15 02:00:00', $model->attr_timestamp); + + // defaultTimeZone different than UTC: + + $val = new DateValidator([ + 'format' => 'php:Y-m-d', + 'timestampAttribute' => 'attr_timestamp', + 'timeZone' => 'Europe/Warsaw', + 'defaultTimeZone' => 'Europe/Warsaw', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2017-06-15'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame(1497477600, $model->attr_timestamp); // = 2017-06-14 22:00:00 UTC = 2017-06-15 00:00:00 Europe/Warsaw + + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd', + 'timestampAttribute' => 'attr_timestamp', + 'defaultTimeZone' => 'Europe/Warsaw', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2017-06-15'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame(1497477600, $model->attr_timestamp); + + // ICU, timeZone => America/Jamaica, timestampAttributeTimeZone => UTC (default) + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'timeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2017-06-15'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2017-06-15 00:00:00', $model->attr_timestamp); + + // PHP, timeZone => America/Jamaica, timestampAttributeTimeZone => UTC (default) + $val = new DateValidator([ + 'format' => 'php:Y-m-d', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'timeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2017-06-15'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2017-06-15 00:00:00', $model->attr_timestamp); + + // ICU, timeZone => UTC (default), timestampAttributeTimeZone => America/Jamaica + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'timeZone' => 'UTC', + 'timestampAttributeTimeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2017-06-15'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2017-06-14 19:00:00', $model->attr_timestamp); + + // PHP, timeZone => UTC (default), timestampAttributeTimeZone => America/Jamaica + $val = new DateValidator([ + 'format' => 'php:Y-m-d', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'timeZone' => 'UTC', + 'timestampAttributeTimeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2017-06-15'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2017-06-14 19:00:00', $model->attr_timestamp); + + // ICU, timeZone => America/Jamaica, timestampAttributeTimeZone => America/Jamaica + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'timeZone' => 'America/Jamaica', + 'timestampAttributeTimeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2017-06-15'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2017-06-14 19:00:00', $model->attr_timestamp); + + // PHP, timeZone => America/Jamaica, timestampAttributeTimeZone => America/Jamaica + $val = new DateValidator([ + 'format' => 'php:Y-m-d', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'timeZone' => 'America/Jamaica', + 'timestampAttributeTimeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2017-06-15'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2017-06-14 19:00:00', $model->attr_timestamp); + + // ICU, defaultTimeZone => America/Jamaica, timestampAttributeTimeZone => UTC (default) + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'defaultTimeZone' => 'America/Jamaica', + 'timestampAttributeTimeZone' => 'UTC', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2020-01-27'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2020-01-27 05:00:00', $model->attr_timestamp); + + // PHP, defaultTimeZone => America/Jamaica, timestampAttributeTimeZone => UTC (default) + $val = new DateValidator([ + 'format' => 'php:Y-m-d', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'defaultTimeZone' => 'America/Jamaica', + 'timestampAttributeTimeZone' => 'UTC', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2020-01-27'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2020-01-27 05:00:00', $model->attr_timestamp); + + // ICU, defaultTimeZone => UTC (default), timestampAttributeTimeZone => America/Jamaica + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'defaultTimeZone' => 'UTC', + 'timestampAttributeTimeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2020-01-27'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2020-01-26 19:00:00', $model->attr_timestamp); + + // PHP, defaultTimeZone => UTC (default), timestampAttributeTimeZone => America/Jamaica + $val = new DateValidator([ + 'format' => 'php:Y-m-d', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'defaultTimeZone' => 'UTC', + 'timestampAttributeTimeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2020-01-27'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2020-01-26 19:00:00', $model->attr_timestamp); + + // ICU, defaultTimeZone => America/Jamaica, timestampAttributeTimeZone => America/Jamaica + $val = new DateValidator([ + 'format' => 'yyyy-MM-dd', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'defaultTimeZone' => 'America/Jamaica', + 'timestampAttributeTimeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2020-01-27'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2020-01-27 00:00:00', $model->attr_timestamp); + + // PHP, defaultTimeZone => America/Jamaica, timestampAttributeTimeZone => America/Jamaica + $val = new DateValidator([ + 'format' => 'php:Y-m-d', + 'timestampAttribute' => 'attr_timestamp', + 'timestampAttributeFormat' => 'yyyy-MM-dd HH:mm:ss', + 'defaultTimeZone' => 'America/Jamaica', + 'timestampAttributeTimeZone' => 'America/Jamaica', + ]); + $model = new FakedValidationModel(); + $model->attr_date = '2020-01-27'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertSame('2020-01-27 00:00:00', $model->attr_timestamp); } /** diff --git a/tests/framework/validators/EachValidatorTest.php b/tests/framework/validators/EachValidatorTest.php index e53d6f3..52c4370 100644 --- a/tests/framework/validators/EachValidatorTest.php +++ b/tests/framework/validators/EachValidatorTest.php @@ -7,11 +7,12 @@ namespace yiiunit\framework\validators; -use yii\db\ArrayExpression; use yii\validators\EachValidator; use yiiunit\data\base\ArrayAccessObject; -use yiiunit\data\base\TraversableObject; +use yiiunit\data\base\Speaker; use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\data\validators\models\ValidatorTestTypedPropModel; +use yiiunit\data\validators\models\ValidatorTestEachAndInlineMethodModel; use yiiunit\TestCase; /** @@ -200,4 +201,68 @@ class EachValidatorTest extends TestCase $this->assertTrue($validator->validate($model->attr_array)); } + + /** + * @see https://github.com/yiisoft/yii2/issues/17810 + * + * Do not reuse model property for storing value + * of different type during validation. + * (ie: public array $dummy; where $dummy is array of booleans, + * validator will try to assign these booleans one by one to $dummy) + */ + public function testTypedProperties() + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Can not be tested on PHP < 7.4'); + return; + } + + $model = new ValidatorTestTypedPropModel(); + + $validator = new EachValidator(['rule' => ['boolean']]); + $validator->validateAttribute($model, 'arrayTypedProperty'); + $this->assertFalse($model->hasErrors('arrayTypedProperty')); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18011 + */ + public function testErrorMessage() + { + $model = new Speaker(); + $model->customLabel = ['invalid_ip']; + + $validator = new EachValidator(['rule' => ['ip']]); + $validator->validateAttribute($model, 'customLabel'); + $validator->validateAttribute($model, 'firstName'); + + $this->assertEquals('This is the custom label must be a valid IP address.', $model->getFirstError('customLabel')); + $this->assertEquals('First Name is invalid.', $model->getFirstError('firstName')); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18051 + */ + public function testCustomMethod() + { + $model = new Speaker(); + $model->firstName = ['a', 'b']; + + $validator = new EachValidator(['rule' => ['customValidatingMethod']]); + $validator->validateAttribute($model, 'firstName'); + + $this->assertEquals('Custom method error', $model->getFirstError('firstName')); + // make sure each value of attribute array is checked separately + $this->assertEquals(['a', 'b'], $model->getCheckedValues()); + // make sure original array is restored at the end + $this->assertEquals(['a', 'b'], $model->firstName); + } + + public function testAnonymousMethod() + { + $model = new ValidatorTestEachAndInlineMethodModel(); + + $model->validate(); + $this->assertFalse($model->hasErrors('arrayProperty')); + } } diff --git a/tests/framework/validators/ExistValidatorTest.php b/tests/framework/validators/ExistValidatorTest.php index d55097e..472c74e 100644 --- a/tests/framework/validators/ExistValidatorTest.php +++ b/tests/framework/validators/ExistValidatorTest.php @@ -198,7 +198,7 @@ abstract class ExistValidatorTest extends DatabaseTestCase { $val = new ExistValidator([ 'targetClass' => OrderItem::className(), - 'targetAttribute' => ['id' => 'COALESCE(order_id, 0)'], + 'targetAttribute' => ['id' => 'COALESCE([[order_id]], 0)'], ]); $m = new Order(['id' => 1]); @@ -235,7 +235,7 @@ abstract class ExistValidatorTest extends DatabaseTestCase $val->validateAttribute($m, 'id'); $this->assertTrue($m->hasErrors('id')); } - + public function testForceMaster() { $connection = $this->getConnectionWithInvalidSlave(); diff --git a/tests/framework/validators/FileValidatorTest.php b/tests/framework/validators/FileValidatorTest.php index c3f490b..cbed1fb 100644 --- a/tests/framework/validators/FileValidatorTest.php +++ b/tests/framework/validators/FileValidatorTest.php @@ -452,6 +452,53 @@ class FileValidatorTest extends TestCase $this->assertNotFalse(stripos(current($m->getErrors('attr_exe')), 'Only files with these extensions ')); } + public function testValidateEmptyExtension() + { + $val = new FileValidator([ + 'extensions' => ['txt', ''], + 'checkExtensionByMimeType' => false, + ]); + $m = FakedValidationModel::createWithAttributes( + [ + 'attr_txt' => $this->createTestFiles([['name' => 'one.txt']]), + 'attr_empty' => $this->createTestFiles([['name' => 'bad.']]), + 'attr_empty2' => $this->createTestFiles([['name' => 'bad']]), + ] + ); + $val->validateAttribute($m, 'attr_txt'); + $this->assertFalse($m->hasErrors('attr_txt')); + $val->validateAttribute($m, 'attr_empty'); + $this->assertFalse($m->hasErrors('attr_empty')); + $val->validateAttribute($m, 'attr_empty2'); + $this->assertFalse($m->hasErrors('attr_empty2')); + } + + public function testValidateAttributeDoubleType() + { + $val = new FileValidator([ + 'extensions' => 'tar.gz, tar.xz', + 'checkExtensionByMimeType' => false, + ]); + + $m = FakedValidationModel::createWithAttributes( + [ + 'attr_tar' => $this->createTestFiles([['name' => 'one.tar.gz']]), + 'attr_bar' => $this->createTestFiles([['name' => 'bad.bar.xz']]), + 'attr_badtar' => $this->createTestFiles([['name' => 'badtar.xz']]), + ] + ); + $val->validateAttribute($m, 'attr_tar'); + $this->assertFalse($m->hasErrors('attr_tar')); + + $val->validateAttribute($m, 'attr_bar'); + $this->assertTrue($m->hasErrors('attr_bar')); + $this->assertNotFalse(stripos(current($m->getErrors('attr_bar')), 'Only files with these extensions ')); + + $val->validateAttribute($m, 'attr_badtar'); + $this->assertTrue($m->hasErrors('attr_badtar')); + $this->assertNotFalse(stripos(current($m->getErrors('attr_badtar')), 'Only files with these extensions ')); + } + public function testIssue11012() { $baseName = '飛兒樂團光茫'; @@ -496,6 +543,7 @@ class FileValidatorTest extends TestCase ['test.txt', 'text/*', 'txt'], ['test.xml', '*/xml', 'xml'], ['test.odt', 'application/vnd*', 'odt'], + ['test.tar.xz', 'application/x-xz', 'tar.xz'], ]); } diff --git a/tests/framework/validators/IpValidatorTest.php b/tests/framework/validators/IpValidatorTest.php index 4895441..7d22c56 100644 --- a/tests/framework/validators/IpValidatorTest.php +++ b/tests/framework/validators/IpValidatorTest.php @@ -303,7 +303,7 @@ class IpValidatorTest extends TestCase $this->assertTrue($validator->validate('8.8.8.8')); $validator->subnet = null; - $validator->ranges = ['10.0.1.0/24', '2001:db0:1:2::/64', 'localhost', '!all']; + $validator->ranges = ['10.0.1.0/24', '2001:db0:1:2::/64', 'localhost', '!any']; $this->assertTrue($validator->validate('10.0.1.2')); $this->assertTrue($validator->validate('2001:db0:1:2::7')); $this->assertTrue($validator->validate('127.0.0.1')); diff --git a/tests/framework/validators/StringValidatorTest.php b/tests/framework/validators/StringValidatorTest.php index d858c39..42413fe 100644 --- a/tests/framework/validators/StringValidatorTest.php +++ b/tests/framework/validators/StringValidatorTest.php @@ -130,4 +130,29 @@ class StringValidatorTest extends TestCase $errorMsg = $model->getErrors('attr_string'); $this->assertEquals('attr_string to short. Min is 5', $errorMsg[0]); } + + /** + * @see https://github.com/yiisoft/yii2/issues/13327 + */ + public function testValidateValueInNonStrictMode() + { + $val = new StringValidator(); + $val->strict = false; + + // string + $this->assertTrue($val->validate('Just some string')); + + // non-scalar + $this->assertFalse($val->validate(['array'])); + $this->assertFalse($val->validate(new \stdClass())); + $this->assertFalse($val->validate(null)); + + // bool + $this->assertTrue($val->validate(true)); + $this->assertTrue($val->validate(false)); + + // number + $this->assertTrue($val->validate(42)); + $this->assertTrue($val->validate(36.6)); + } } diff --git a/tests/framework/validators/UniqueValidatorTest.php b/tests/framework/validators/UniqueValidatorTest.php index ea673e2..fac5948 100644 --- a/tests/framework/validators/UniqueValidatorTest.php +++ b/tests/framework/validators/UniqueValidatorTest.php @@ -109,6 +109,10 @@ abstract class UniqueValidatorTest extends DatabaseTestCase $val->validateAttribute($m, 'ref'); $this->assertTrue($m->hasErrors('ref')); $m = new ValidatorTestRefModel(); + // Add id manual, there is no definition of sequence for the table. + if ($this->driverName === 'oci') { + $m->id = 7; + } $m->ref = 12121; $val->validateAttribute($m, 'ref'); $this->assertFalse($m->hasErrors('ref')); @@ -282,6 +286,12 @@ abstract class UniqueValidatorTest extends DatabaseTestCase $val = new UniqueValidator(); $m = new ValidatorTestMainModel(['field1' => '']); + + // Add id manual, there is no definition of sequence for the table. + if ($this->driverName === 'oci') { + $m->id = 5; + } + $val->validateAttribute($m, 'field1'); $this->assertFalse($m->hasErrors('field1')); $m->save(false); @@ -298,6 +308,12 @@ abstract class UniqueValidatorTest extends DatabaseTestCase $val = new UniqueValidator(); $m = new ValidatorTestRefModel(['ref' => 0]); + + // Add id manual, there is no definition of sequence for the table. + if ($this->driverName === 'oci') { + $m->id = 6; + } + $val->validateAttribute($m, 'ref'); $this->assertFalse($m->hasErrors('ref')); $m->save(false); @@ -416,7 +432,7 @@ abstract class UniqueValidatorTest extends DatabaseTestCase { $validator = new UniqueValidator([ 'targetAttribute' => [ - 'title' => 'LOWER(title)', + 'title' => 'LOWER([[title]])', ], ]); $model = new Document(); @@ -445,7 +461,7 @@ abstract class UniqueValidatorTest extends DatabaseTestCase $this->fail('Query is crashed because "with" relation cannot be loaded'); } } - + /** * Test join with doesn't attempt to eager load joinWith relations * @see https://github.com/yiisoft/yii2/issues/17389 @@ -463,7 +479,7 @@ abstract class UniqueValidatorTest extends DatabaseTestCase $this->fail('Query is crashed because "joinWith" relation cannot be loaded'); } } - + public function testForceMaster() { $connection = $this->getConnectionWithInvalidSlave(); @@ -504,9 +520,9 @@ class WithCustomer extends Customer { class JoinWithCustomer extends Customer { public static function find() { $res = parent::find(); - + $res->joinWith('profile'); - + return $res; } } diff --git a/tests/framework/validators/ValidatorTest.php b/tests/framework/validators/ValidatorTest.php index 063b81e..a7b22e5 100644 --- a/tests/framework/validators/ValidatorTest.php +++ b/tests/framework/validators/ValidatorTest.php @@ -71,7 +71,7 @@ class ValidatorTest extends TestCase $this->assertSame(['c', 'd', 'e'], $val->except); $val = TestValidator::createValidator('inlineVal', $model, ['val_attr_a'], ['params' => ['foo' => 'bar']]); $this->assertInstanceOf(InlineValidator::className(), $val); - $this->assertSame('inlineVal', $val->method); + $this->assertSame('inlineVal', $val->method[1]); $this->assertSame(['foo' => 'bar'], $val->params); } @@ -203,12 +203,14 @@ class ValidatorTest extends TestCase // Access to validator in inline validation (https://github.com/yiisoft/yii2/issues/6242) $model = new FakedValidationModel(); + $model->val_attr_a = 'a'; $val = Validator::createValidator('inlineVal', $model, ['val_attr_a'], ['params' => ['foo' => 'bar']]); $val->validateAttribute($model, 'val_attr_a'); $args = $model->getInlineValArgs(); - $this->assertCount(3, $args); + $this->assertCount(4, $args); $this->assertEquals('val_attr_a', $args[0]); + $this->assertEquals('a', $args[3]); $this->assertEquals(['foo' => 'bar'], $args[1]); $this->assertInstanceOf(InlineValidator::className(), $args[2]); } @@ -227,7 +229,7 @@ class ValidatorTest extends TestCase $val->clientValidate = 'clientInlineVal'; $args = $val->clientValidateAttribute($model, 'val_attr_a', null); - $this->assertCount(3, $args); + $this->assertCount(4, $args); $this->assertEquals('val_attr_a', $args[0]); $this->assertEquals(['foo' => 'bar'], $args[1]); $this->assertInstanceOf(InlineValidator::className(), $args[2]); @@ -317,7 +319,7 @@ class ValidatorTest extends TestCase $model = new DynamicModel(); $model->defineAttribute(1); $model->addRule([1], SafeValidator::className()); - + $this->assertNull($model->{1}); $this->assertTrue($model->validate([1])); diff --git a/tests/framework/validators/data/mimeType/test.tar.xz b/tests/framework/validators/data/mimeType/test.tar.xz new file mode 100644 index 0000000..4d2da41 Binary files /dev/null and b/tests/framework/validators/data/mimeType/test.tar.xz differ diff --git a/tests/framework/web/AssetBundleTest.php b/tests/framework/web/AssetBundleTest.php index 935761b..e1a0234 100644 --- a/tests/framework/web/AssetBundleTest.php +++ b/tests/framework/web/AssetBundleTest.php @@ -527,7 +527,6 @@ EOF; } Yii::setAlias('@web', $webAlias); - $view = $this->getView(['appendTimestamp' => $appendTimestamp]); $method = 'register' . ucfirst($type) . 'File'; $view->$method($path); @@ -542,7 +541,7 @@ EOF; $view = $this->getView(); $am = $view->assetManager; - // publising without timestamp + // publishing without timestamp $result = $am->publish($path . '/data.txt'); $this->assertRegExp('/.*data.txt$/i', $result[1]); unset($view, $am, $result); @@ -554,6 +553,21 @@ EOF; $result = $am->publish($path . '/data.txt'); $this->assertRegExp('/.*data.txt\?v=\d+$/i', $result[1]); } + + /** + * @see https://github.com/yiisoft/yii2/issues/18529 + */ + public function testNonRelativeAssetWebPathWithTimestamp() + { + Yii::setAlias('@webroot', '@yiiunit/data/web/assetSources/'); + + $view = $this->getView(['appendTimestamp' => true]); + TestNonRelativeAsset::register($view); + $this->assertRegExp( + '~123 + $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); + $this->assertRegExp($pattern, $html); + + // test append timestamp when @web has the same name as the asset-source folder + \Yii::setAlias('@web', '/assetSources/'); + $view = new View(); + $view->registerJsFile(\Yii::getAlias('@web/assetSources/js/jquery.js'), + ['depends' => 'yii\web\AssetBundle']); // + $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); + $this->assertRegExp($pattern, $html); + // reset aliases + $this->setUpAliases(); + // won't be used AssetManager but the timestamp will be $view = new View(); $view->registerJsFile('/assetSources/js/jquery.js'); // @@ -211,7 +229,7 @@ class ViewTest extends TestCase $this->assertRegExp($pattern, $html); // with alias but wo timestamp - // The timestamp setting won't be redefined because global AssetManager is used + // redefine AssetManager timestamp setting $view = new View(); $view->registerJsFile('@web/assetSources/js/jquery.js', [ @@ -219,7 +237,7 @@ class ViewTest extends TestCase 'depends' => 'yii\web\AssetBundle', ]); // $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); - $this->assertRegExp($pattern, $html); + $this->assertNotRegExp($pattern, $html); // wo depends == wo AssetManager $view = new View(); @@ -268,15 +286,15 @@ class ViewTest extends TestCase $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); $this->assertRegExp($pattern, $html); - // The timestamp setting won't be redefined because global AssetManager is used + // redefine AssetManager timestamp setting $view = new View(); $view->registerJsFile('/assetSources/js/jquery.js', [ 'appendTimestamp' => true, 'depends' => 'yii\web\AssetBundle', - ]); // + ]); // $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); - $this->assertNotRegExp($pattern, $html); + $this->assertRegExp($pattern, $html); $view = new View(); $view->registerJsFile('/assetSources/js/jquery.js', @@ -331,6 +349,24 @@ class ViewTest extends TestCase $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); $this->assertRegExp($pattern, $html); + // test append timestamp when @web is prefixed in url + \Yii::setAlias('@web', '/test-app'); + $view = new View(); + $view->registerCssFile(\Yii::getAlias('@web/assetSources/css/stub.css'), + ['depends' => 'yii\web\AssetBundle']); // + $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); + $this->assertRegExp($pattern, $html); + + // test append timestamp when @web has the same name as the asset-source folder + \Yii::setAlias('@web', '/assetSources/'); + $view = new View(); + $view->registerCssFile(\Yii::getAlias('@web/assetSources/css/stub.css'), + ['depends' => 'yii\web\AssetBundle']); // + $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); + $this->assertRegExp($pattern, $html); + // reset aliases + $this->setUpAliases(); + // won't be used AssetManager but the timestamp will be $view = new View(); $view->registerCssFile('/assetSources/css/stub.css'); // @@ -357,7 +393,7 @@ class ViewTest extends TestCase $this->assertRegExp($pattern, $html); // with alias but wo timestamp - // The timestamp setting won't be redefined because global AssetManager is used + // redefine AssetManager timestamp setting $view = new View(); $view->registerCssFile('@web/assetSources/css/stub.css', [ @@ -365,7 +401,7 @@ class ViewTest extends TestCase 'depends' => 'yii\web\AssetBundle', ]); // $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); - $this->assertRegExp($pattern, $html); + $this->assertNotRegExp($pattern, $html); // wo depends == wo AssetManager $view = new View(); @@ -414,15 +450,15 @@ class ViewTest extends TestCase $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); $this->assertRegExp($pattern, $html); - // The timestamp setting won't be redefined because global AssetManager is used + // redefine AssetManager timestamp setting $view = new View(); $view->registerCssFile('/assetSources/css/stub.css', [ 'appendTimestamp' => true, 'depends' => 'yii\web\AssetBundle', - ]); // + ]); // $html = $view->render('@yiiunit/data/views/layout.php', ['content' => 'content']); - $this->assertNotRegExp($pattern, $html); + $this->assertRegExp($pattern, $html); $view = new View(); $view->registerCssFile('/assetSources/css/stub.css', diff --git a/tests/framework/web/session/AbstractDbSessionTest.php b/tests/framework/web/session/AbstractDbSessionTest.php index a2a4375..835ea13 100644 --- a/tests/framework/web/session/AbstractDbSessionTest.php +++ b/tests/framework/web/session/AbstractDbSessionTest.php @@ -9,8 +9,8 @@ namespace yiiunit\framework\web\session; use Yii; use yii\db\Connection; -use yii\db\Query; use yii\db\Migration; +use yii\db\Query; use yii\web\DbSession; use yiiunit\framework\console\controllers\EchoMigrateController; use yiiunit\TestCase; @@ -20,6 +20,8 @@ use yiiunit\TestCase; */ abstract class AbstractDbSessionTest extends TestCase { + use SessionTestTrait; + /** * @return string[] the driver names that are suitable for the test (mysql, pgsql, etc) */ @@ -159,6 +161,7 @@ abstract class AbstractDbSessionTest extends TestCase // add mapped custom column $migration = new Migration; + $migration->compact = true; $migration->addColumn($session->sessionTable, 'user_id', $migration->integer()); $session->writeCallback = function ($session) { @@ -265,4 +268,14 @@ abstract class AbstractDbSessionTest extends TestCase Yii::$app->set('sessionDb', null); ini_set('session.gc_maxlifetime', $oldTimeout); } + + public function testInitUseStrictMode() + { + $this->initStrictModeTest(DbSession::className()); + } + + public function testUseStrictMode() + { + $this->useStrictModeTest(DbSession::className()); + } } diff --git a/tests/framework/web/session/CacheSessionTest.php b/tests/framework/web/session/CacheSessionTest.php index 018d164..e31b0f9 100644 --- a/tests/framework/web/session/CacheSessionTest.php +++ b/tests/framework/web/session/CacheSessionTest.php @@ -16,6 +16,8 @@ use yii\web\CacheSession; */ class CacheSessionTest extends \yiiunit\TestCase { + use SessionTestTrait; + protected function setUp() { parent::setUp(); @@ -51,4 +53,14 @@ class CacheSessionTest extends \yiiunit\TestCase $this->assertTrue($session->destroySession($session->getId())); } + + public function testInitUseStrictMode() + { + $this->initStrictModeTest(CacheSession::className()); + } + + public function testUseStrictMode() + { + $this->useStrictModeTest(CacheSession::className()); + } } diff --git a/tests/framework/web/session/SessionTest.php b/tests/framework/web/session/SessionTest.php index 28fc1e1..6ad327f 100644 --- a/tests/framework/web/session/SessionTest.php +++ b/tests/framework/web/session/SessionTest.php @@ -15,6 +15,8 @@ use yiiunit\TestCase; */ class SessionTest extends TestCase { + use SessionTestTrait; + /** * Test to prove that after Session::destroy session id set to old value. */ @@ -69,6 +71,7 @@ class SessionTest extends TestCase $newGcProbability = $session->getGCProbability(); $this->assertNotEquals($oldGcProbability, $newGcProbability); $this->assertEquals(100, $newGcProbability); + $session->setGCProbability($oldGcProbability); } /** @@ -88,4 +91,24 @@ class SessionTest extends TestCase $session->destroy(); } + + public function testInitUseStrictMode() + { + $this->initStrictModeTest(Session::className()); + } + + public function testUseStrictMode() + { + //Manual garbage collection since native storage module might not support removing data via Session::destroySession() + $sessionSavePath = session_save_path() ?: sys_get_temp_dir(); + // Only perform garbage collection if "N argument" is not used, + // see https://www.php.net/manual/en/session.configuration.php#ini.session.save-path + if (strpos($sessionSavePath, ';') === false) { + foreach (['non-existing-non-strict', 'non-existing-strict'] as $sessionId) { + @unlink($sessionSavePath . '/sess_' . $sessionId); + } + } + + $this->useStrictModeTest(Session::className()); + } } diff --git a/tests/framework/web/session/SessionTestTrait.php b/tests/framework/web/session/SessionTestTrait.php new file mode 100644 index 0000000..1c3e3da --- /dev/null +++ b/tests/framework/web/session/SessionTestTrait.php @@ -0,0 +1,73 @@ +useStrictMode = false; + $this->assertEquals(false, $session->getUseStrictMode()); + + if (PHP_VERSION_ID < 50502 && !$session->getUseCustomStorage()) { + $this->expectException('yii\base\InvalidConfigException'); + $session->useStrictMode = true; + return; + } + + $session->useStrictMode = true; + $this->assertEquals(true, $session->getUseStrictMode()); + } + + /** + * @param string $class + */ + protected function useStrictModeTest($class) + { + /** @var Session $session */ + $session = new $class(); + + if (PHP_VERSION_ID < 50502 && !$session->getUseCustomStorage()) { + $this->markTestSkipped('Can not be tested on PHP < 5.5.2 without custom storage class.'); + return; + } + + //non-strict-mode test + $session->useStrictMode = false; + $session->close(); + $session->destroySession('non-existing-non-strict'); + $session->setId('non-existing-non-strict'); + $session->open(); + $this->assertEquals('non-existing-non-strict', $session->getId()); + $session->close(); + + //strict-mode test + $session->useStrictMode = true; + $session->close(); + $session->destroySession('non-existing-strict'); + $session->setId('non-existing-strict'); + $session->open(); + $id = $session->getId(); + $this->assertNotEquals('non-existing-strict', $id); + $session->set('strict_mode_test', 'session data'); + $session->close(); + //Ensure session was not stored under forced id + $session->setId('non-existing-strict'); + $session->open(); + $this->assertNotEquals('session data', $session->get('strict_mode_test')); + $session->close(); + //Ensure session can be accessed with the new (and thus existing) id. + $session->setId($id); + $session->open(); + $this->assertNotEmpty($id); + $this->assertEquals($id, $session->getId()); + $this->assertEquals('session data', $session->get('strict_mode_test')); + $session->close(); + } +} diff --git a/tests/framework/widgets/ActiveFormTest.php b/tests/framework/widgets/ActiveFormTest.php index c8accbb..7990861 100644 --- a/tests/framework/widgets/ActiveFormTest.php +++ b/tests/framework/widgets/ActiveFormTest.php @@ -158,6 +158,7 @@ HTML /** * @see https://github.com/yiisoft/yii2/issues/15476 + * @see https://github.com/yiisoft/yii2/issues/16892 */ public function testValidationStateOnInput() { @@ -182,5 +183,25 @@ HTML EOF , (string) $form->field($model, 'name')); + + $this->assertEqualsWithoutLE(<<<'EOF' +
+ + + +
I have an error!
+
+EOF + , (string) $form->field($model, 'name')->checkbox()); + + $this->assertEqualsWithoutLE(<<<'EOF' +
+ + + +
I have an error!
+
+EOF + , (string) $form->field($model, 'name')->radio()); } } diff --git a/tests/framework/widgets/LinkSorterTest.php b/tests/framework/widgets/LinkSorterTest.php index ae1a75e..8d60722 100644 --- a/tests/framework/widgets/LinkSorterTest.php +++ b/tests/framework/widgets/LinkSorterTest.php @@ -86,7 +86,7 @@ class LinkSorterTest extends DatabaseTestCase public function testShouldTriggerInitEvent() { $initTriggered = false; - $linkSorter = new LinkSorter( + new LinkSorter( [ 'sort' => [ 'attributes' => ['total'], diff --git a/tests/js/tests/yii.activeForm.test.js b/tests/js/tests/yii.activeForm.test.js index 323b74a..50582b5 100644 --- a/tests/js/tests/yii.activeForm.test.js +++ b/tests/js/tests/yii.activeForm.test.js @@ -183,6 +183,28 @@ describe('yii.activeForm', function () { $activeForm.yiiActiveForm('updateAttribute', inputId); assert.equal('New value', eventData.value); }); + + // https://github.com/yiisoft/yii2/issues/8225 + + it('the value of the checkboxes must be an array', function () { + var inputId = 'test_checkbox'; + var $input = $('#' + inputId); + + $activeForm = $('#w1'); + $activeForm.yiiActiveForm('destroy'); + $activeForm.yiiActiveForm([ + { + id: inputId, + input: '#' + inputId + } + ]).on('afterValidateAttribute', afterValidateAttributeSpy); + + $input.find('input').prop('checked', true); + $activeForm.yiiActiveForm('updateAttribute', inputId); + var value = eventData.value; + assert.isArray(value); + assert.deepEqual(['1', '0'], value); + }); }); describe('afterValidate', function () { diff --git a/tests/js/tests/yii.gridView.test.js b/tests/js/tests/yii.gridView.test.js index adee409..bc1feb6 100644 --- a/tests/js/tests/yii.gridView.test.js +++ b/tests/js/tests/yii.gridView.test.js @@ -128,8 +128,7 @@ describe('yii.gridView', function () { * @param $el */ function click($el) { - var e = $.Event('click'); - $el.trigger(e); + $el.click(); } /** diff --git a/tests/js/tests/yii.test.js b/tests/js/tests/yii.test.js index f135edb..0164dcf 100644 --- a/tests/js/tests/yii.test.js +++ b/tests/js/tests/yii.test.js @@ -742,6 +742,7 @@ describe('yii', function () { 'query parameters': ['/posts/index?foo=1&bar=2', {foo: '1', bar: '2'}], 'query parameter with multiple values (not array)': ['/posts/index?foo=1&foo=2', {'foo': ['1', '2']}], 'query parameter with multiple values (array)': ['/posts/index?foo[]=1&foo[]=2', {'foo[]': ['1', '2']}], + 'query parameter with empty value': ['/posts/index?foo=1&foo2', {'foo': '1', 'foo2': ''}], 'anchor': ['/posts/index#post', {}], 'query parameters, anchor': ['/posts/index?foo=1&bar=2#post', {foo: '1', bar: '2'}], 'relative url, query parameters': ['?foo=1&bar=2', {foo: '1', bar: '2'}],