Browse Source

Merge remote-tracking branch 'origin/master' into redis

* origin/master: (104 commits)
  Fixed typo in query builder and reverted changes to DbCache.php
  added note about enabling APC cache for CLI
  more assertions for cache test
  fixed dbcache multiget
  fixed Html test under Windows (line endings)
  fixed DB quoting typo
  create a webapp in test bootstrap
  updated expected exception message
  Added a basic app.
  initial implementation of asset command.
  script WIP
  script command WIP
  Fixed bug in yiic.php. Refactoring AssetConverter.
  adjusted default app layout
  refactoring and documentation for asset/script management.
  Finished asset converter.
  removed unnecessary code, added comment about displaying errors
  better handling of errors during rendering an error
  safer exception rendering
  turn asset manager into a getter.
  ...
tags/2.0.0-beta
Carsten Brandt 12 years ago
parent
commit
3e3290164c
  1. 1
      app/assets/.gitignore
  2. 9
      app/index.php
  3. 15
      app/protected/config/main.php
  4. 22
      app/protected/controllers/SiteController.php
  5. 43
      app/protected/models/User.php
  6. 1
      app/protected/runtime/.gitignore
  7. 22
      app/protected/views/layouts/main.php
  8. 17
      app/protected/views/site/index.php
  9. 1
      build/.htaccess
  10. 20
      build/build
  11. 23
      build/build.bat
  12. 276
      build/build.xml
  13. 112
      build/controllers/LocaleController.php
  14. 26
      docs/api/base/Component.md
  15. 30
      docs/api/base/Object.md
  16. 328
      docs/code_style.md
  17. BIN
      docs/full_2011_11_12.png
  18. BIN
      docs/hierarchy_2011_11_12.png
  19. 192
      docs/review_2011_11_12_alex.md
  20. 379
      framework/YiiBase.php
  21. 5
      framework/assets.php
  22. 4
      framework/base/ActionEvent.php
  23. 269
      framework/base/Application.php
  24. 144
      framework/base/Component.php
  25. 122
      framework/base/Controller.php
  26. 58
      framework/base/ErrorException.php
  27. 30
      framework/base/ErrorHandler.php
  28. 9
      framework/base/Event.php
  29. 5
      framework/base/HttpException.php
  30. 24
      framework/base/Model.php
  31. 53
      framework/base/Module.php
  32. 3
      framework/base/Object.php
  33. 22
      framework/base/Response.php
  34. 37
      framework/base/Theme.php
  35. 26
      framework/base/UnknownClassException.php
  36. 2
      framework/base/UnknownMethodException.php
  37. 519
      framework/base/View.php
  38. 44
      framework/base/ViewEvent.php
  39. 57
      framework/base/Widget.php
  40. 1
      framework/caching/ApcCache.php
  41. 22
      framework/caching/ChainedDependency.php
  42. 6
      framework/caching/DbCache.php
  43. 1
      framework/caching/DbDependency.php
  44. 4
      framework/caching/FileCache.php
  45. 2
      framework/caching/MemCache.php
  46. 2
      framework/caching/ZendDataCache.php
  47. 1
      framework/console/Application.php
  48. 1
      framework/console/Controller.php
  49. 123
      framework/console/controllers/AppController.php
  50. 353
      framework/console/controllers/AssetController.php
  51. 4
      framework/console/controllers/HelpController.php
  52. 8
      framework/console/webapp/default/index.php
  53. 4
      framework/console/webapp/default/protected/config/main.php
  54. 7
      framework/console/webapp/default/protected/views/layouts/main.php
  55. 99
      framework/db/ActiveRecord.php
  56. 90
      framework/db/Command.php
  57. 42
      framework/db/Connection.php
  58. 479
      framework/db/QueryBuilder.php
  59. 16
      framework/db/Schema.php
  60. 23
      framework/db/StaleObjectException.php
  61. 6
      framework/db/Transaction.php
  62. 323
      framework/helpers/ArrayHelper.php
  63. 449
      framework/helpers/ConsoleColor.php
  64. 255
      framework/helpers/FileHelper.php
  65. 965
      framework/helpers/Html.php
  66. 245
      framework/helpers/SecurityHelper.php
  67. 108
      framework/helpers/StringHelper.php
  68. 108
      framework/helpers/VarDumper.php
  69. 340
      framework/helpers/base/ArrayHelper.php
  70. 470
      framework/helpers/base/ConsoleColor.php
  71. 172
      framework/helpers/base/FileHelper.php
  72. 981
      framework/helpers/base/Html.php
  73. 272
      framework/helpers/base/SecurityHelper.php
  74. 125
      framework/helpers/base/StringHelper.php
  75. 134
      framework/helpers/base/VarDumper.php
  76. 0
      framework/helpers/base/mimeTypes.php
  77. 97
      framework/i18n/I18N.php
  78. 2
      framework/i18n/PhpMessageSource.php
  79. 627
      framework/i18n/data/plurals.php
  80. 109
      framework/i18n/data/plurals.xml
  81. 3
      framework/logging/DbTarget.php
  82. 24
      framework/logging/FileTarget.php
  83. 3
      framework/logging/Target.php
  84. 39
      framework/validators/BooleanValidator.php
  85. 64
      framework/validators/CaptchaValidator.php
  86. 185
      framework/validators/CompareValidator.php
  87. 64
      framework/validators/DateValidator.php
  88. 15
      framework/validators/DefaultValueValidator.php
  89. 33
      framework/validators/EmailValidator.php
  90. 58
      framework/validators/ExistValidator.php
  91. 277
      framework/validators/FileValidator.php
  92. 20
      framework/validators/FilterValidator.php
  93. 21
      framework/validators/InlineValidator.php
  94. 76
      framework/validators/NumberValidator.php
  95. 58
      framework/validators/RangeValidator.php
  96. 57
      framework/validators/RegularExpressionValidator.php
  97. 52
      framework/validators/RequiredValidator.php
  98. 91
      framework/validators/StringValidator.php
  99. 29
      framework/validators/UniqueValidator.php
  100. 40
      framework/validators/UrlValidator.php
  101. Some files were not shown because too many files have changed in this diff Show More

1
app/assets/.gitignore vendored

@ -0,0 +1 @@
*

9
app/index.php

@ -0,0 +1,9 @@
<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
require(__DIR__ . '/../framework/yii.php');
$config = require(__DIR__ . '/protected/config/main.php');
$application = new yii\web\Application($config);
$application->run();

15
app/protected/config/main.php

@ -0,0 +1,15 @@
<?php
return array(
'id' => 'hello',
'basePath' => dirname(__DIR__),
'components' => array(
'cache' => array(
'class' => 'yii\caching\FileCache',
),
'user' => array(
'class' => 'yii\web\User',
'identityClass' => 'app\models\User',
)
),
);

22
app/protected/controllers/SiteController.php

@ -0,0 +1,22 @@
<?php
class SiteController extends \yii\web\Controller
{
public function actionIndex()
{
echo $this->render('index');
}
public function actionLogin()
{
$user = app\models\User::findIdentity(100);
Yii::$app->getUser()->login($user);
Yii::$app->getResponse()->redirect(array('site/index'));
}
public function actionLogout()
{
Yii::$app->getUser()->logout();
Yii::$app->getResponse()->redirect(array('site/index'));
}
}

43
app/protected/models/User.php

@ -0,0 +1,43 @@
<?php
namespace app\models;
class User extends \yii\base\Object implements \yii\web\Identity
{
public $id;
public $name;
public $authKey;
private static $users = array(
'100' => array(
'id' => '100',
'authKey' => 'test100key',
'name' => 'admin',
),
'101' => array(
'id' => '101',
'authKey' => 'test101key',
'name' => 'demo',
),
);
public static function findIdentity($id)
{
return isset(self::$users[$id]) ? new self(self::$users[$id]) : null;
}
public function getId()
{
return $this->id;
}
public function getAuthKey()
{
return $this->authKey;
}
public function validateAuthKey($authKey)
{
return $this->authKey === $authKey;
}
}

1
app/protected/runtime/.gitignore vendored

@ -0,0 +1 @@
*

22
app/protected/views/layouts/main.php

@ -0,0 +1,22 @@
<?php
/**
* @var $this \yii\base\View
* @var $content string
*/
use yii\helpers\Html;
?>
<!DOCTYPE html>
<html>
<?php $this->beginPage(); ?>
<head>
<title><?php echo Html::encode($this->title); ?></title>
<?php $this->head(); ?>
</head>
<body>
<h1>Welcome</h1>
<?php $this->beginBody(); ?>
<?php echo $content; ?>
<?php $this->endBody(); ?>
</body>
<?php $this->endPage(); ?>
</html>

17
app/protected/views/site/index.php

@ -0,0 +1,17 @@
<?php
/** @var $this \yii\base\View */
use yii\helpers\Html;
$this->title = 'Hello World';
$user = Yii::$app->getUser();
if ($user->isGuest) {
echo Html::a('login', array('login'));
} else {
echo "You are logged in as " . $user->identity->name . "<br/>";
echo Html::a('logout', array('logout'));
}
?>

1
build/.htaccess

@ -0,0 +1 @@
deny from all

20
build/build

@ -0,0 +1,20 @@
#!/usr/bin/env php
<?php
/**
* build script file.
*
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
// fcgi doesn't have STDIN defined by default
defined('STDIN') or define('STDIN', fopen('php://stdin', 'r'));
require(__DIR__ . '/../framework/yii.php');
$id = 'yiic-build';
$basePath = __DIR__;
$application = new yii\console\Application($id, $basePath);
$application->run();

23
build/build.bat

@ -0,0 +1,23 @@
@echo off
rem -------------------------------------------------------------
rem build script for Windows.
rem
rem This is the bootstrap script for running build on Windows.
rem
rem @author Qiang Xue <qiang.xue@gmail.com>
rem @link http://www.yiiframework.com/
rem @copyright 2008 Yii Software LLC
rem @license http://www.yiiframework.com/license/
rem @version $Id$
rem -------------------------------------------------------------
@setlocal
set BUILD_PATH=%~dp0
if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
%PHP_COMMAND% "%BUILD_PATH%build" %*
@endlocal

276
build/build.xml

@ -0,0 +1,276 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Phing build file for Yii.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright 2008-2009 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
-->
<project name="yii" basedir="." default="help">
<!-- task definitions -->
<taskdef name="yii-init-build" classname="YiiInitTask" classpath="tasks" />
<!--
<taskdef name="yii-pear" classname="YiiPearTask" classpath="tasks"/>
-->
<!-- init yii.version, yii.revision and yii.winbuild -->
<yii-init-build />
<!-- these are required external commands -->
<property name="php" value="php" /> <!-- PHP parser -->
<property name="hhc" value="hhc" /> <!-- compile phpdoc into CHM -->
<property name="pdflatex" value="pdflatex" /> <!-- generates PDF from LaTex -->
<property name="pkgname" value="${phing.project.name}-${yii.version}.${yii.revision}"/>
<property name="docname" value="${phing.project.name}-docs-${yii.version}.${yii.revision}"/>
<property name="pearname" value="${phing.project.name}-${yii.release}.tgz" />
<!-- directory definitions -->
<property name="build.base.dir" value="release"/>
<property name="build.dist.dir" value="${build.base.dir}/dist"/>
<property name="build.src.dir" value="${build.base.dir}/${pkgname}"/>
<property name="build.pear.src.dir" value="${build.src.dir}/framework" />
<property name="build.doc.dir" value="${build.base.dir}/${docname}"/>
<property name="build.web.dir" value="${build.base.dir}/web"/>
<tstamp>
<format property="DATE" pattern="%b %e %Y" />
</tstamp>
<if>
<equals arg1="${yii.winbuild}" arg2="true"/>
<then>
<property name="build" value="build"/>
</then>
<else>
<property name="build" value="php build"/>
</else>
</if>
<!-- source files in the framework -->
<fileset dir=".." id="framework">
<exclude name="**/.gitignore"/>
<exclude name="**/*.bak"/>
<exclude name="**/*~"/>
<include name="framework/**/*"/>
<include name="requirements/**/*"/>
<include name="demos/**/*"/>
<include name="CHANGELOG"/>
<include name="UPGRADE"/>
<include name="LICENSE"/>
<include name="README"/>
</fileset>
<!-- doc files -->
<fileset dir="../docs" id="docs">
<exclude name="**/.gitignore"/>
<exclude name="**/*.bak"/>
<exclude name="**/*~"/>
<include name="guide/**/*"/>
<include name="blog/**/*"/>
</fileset>
<fileset dir="../docs/guide" id="docs-guide">
<exclude name="**/.gitignore"/>
<exclude name="**/*.bak"/>
<exclude name="**/*~"/>
<include name="**/*"/>
</fileset>
<fileset dir="../docs/blog" id="docs-blog">
<exclude name="**/.gitignore"/>
<exclude name="**/*.bak"/>
<exclude name="**/*~"/>
<include name="**/*"/>
</fileset>
<fileset dir="." id="writables">
<include name="${build.src.dir}/**/runtime" />
<include name="${build.src.dir}/**/assets" />
<include name="${build.src.dir}/demos/**/data" />
</fileset>
<fileset dir="." id="executables">
<include name="${build.src.dir}/**/yiic" />
</fileset>
<target name="src" depends="sync">
<echo>Building package ${pkgname}...</echo>
<echo>Copying files to build directory...</echo>
<copy todir="${build.src.dir}">
<fileset refid="framework"/>
</copy>
<echo>Changing file permissions...</echo>
<chmod mode="0777">
<fileset refid="writables" />
</chmod>
<chmod mode="0755">
<fileset refid="executables" />
</chmod>
<echo>Generating source release file...</echo>
<mkdir dir="${build.dist.dir}" />
<if>
<equals arg1="${yii.winbuild}" arg2="true"/>
<then>
<tar destfile="${build.dist.dir}/${pkgname}.tar.gz" compression="gzip">
<fileset dir="${build.base.dir}">
<include name="${pkgname}/**/*"/>
</fileset>
</tar>
</then>
<else>
<exec command="tar czpf ${pkgname}.tar.gz ${pkgname}" dir="${build.base.dir}"/>
<move file="${build.base.dir}/${pkgname}.tar.gz" todir="${build.dist.dir}" />
</else>
</if>
<zip destfile="${build.dist.dir}/${pkgname}.zip">
<fileset dir="${build.base.dir}">
<include name="${pkgname}/**/*"/>
</fileset>
</zip>
</target>
<target name="doc" depends="sync">
<echo>Building documentation...</echo>
<echo>Building Guide PDF...</echo>
<exec command="${build} guideLatex" dir="." passthru="true" />
<exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guide" passthru="true"/>
<exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guide" passthru="true"/>
<exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guide" passthru="true"/>
<move file="commands/guide/guide.pdf" tofile="${build.doc.dir}/yii-guide-${yii.version}.pdf" />
<echo>Building Blog PDF...</echo>
<exec command="${build} blogLatex" dir="." passthru="true" />
<exec command="${pdflatex} blog.tex -interaction=nonstopmode -max-print-line=120" dir="commands/blog" passthru="true"/>
<exec command="${pdflatex} blog.tex -interaction=nonstopmode -max-print-line=120" dir="commands/blog" passthru="true"/>
<exec command="${pdflatex} blog.tex -interaction=nonstopmode -max-print-line=120" dir="commands/blog" passthru="true"/>
<move file="commands/blog/blog.pdf" tofile="${build.doc.dir}/yii-blog-${yii.version}.pdf" />
<echo>Building API...</echo>
<exec command="${build} api ${build.doc.dir}" dir="." passthru="true" />
<!--
<echo>Building API CHM...</echo>
<exec command="${hhc} ${build.doc.dir}/api/manual.hhp" />
<move file="${build.doc.dir}/api/manual.chm" tofile="${build.doc.dir}/yii-api-${yii.version}.chm" />
<delete>
<fileset dir="${build.doc.dir}/api">
<include name="manual.*" />
</fileset>
</delete>
-->
<echo>Generating doc release file...</echo>
<mkdir dir="${build.dist.dir}" />
<tar destfile="${build.dist.dir}/${docname}.tar.gz" compression="gzip">
<fileset dir="${build.base.dir}">
<include name="${docname}/**/*"/>
</fileset>
</tar>
<zip destfile="${build.dist.dir}/${docname}.zip">
<fileset dir="${build.base.dir}">
<include name="${docname}/**/*"/>
</fileset>
</zip>
</target>
<target name="web" depends="sync">
<echo>Building online API...</echo>
<mkdir dir="${build.web.dir}/common/data/${yii.version}" />
<exec command="${build} api ${build.web.dir}/common/data/${yii.version} online" dir="." passthru="true" />
<echo>Copying tutorials...</echo>
<copy todir="${build.web.dir}/common/data/${yii.version}/tutorials/guide">
<fileset refid="docs-guide"/>
</copy>
<copy todir="${build.web.dir}/common/data/${yii.version}/tutorials/blog">
<fileset refid="docs-blog"/>
</copy>
<echo>Copying release text files...</echo>
<mkdir dir="${build.web.dir}/frontend/www/files" />
<copy file="../CHANGELOG" tofile="${build.web.dir}/frontend/www/files/CHANGELOG-${yii.version}.txt" />
<copy file="../UPGRADE" tofile="${build.web.dir}/frontend/www/files/UPGRADE-${yii.version}.txt" />
<echo>
Finished building Web files.
Please update yiisite/common/data/versions.php file with the following code:
'1.1'=>array(
'version'=>'${yii.version}',
'revision'=>'${yii.revision}',
'date'=>'${yii.date}',
'latest'=>true,
),
</echo>
</target>
<target name="sync">
<echo>Synchronizing code changes for ${pkgname}...</echo>
<echo>Building autoload map...</echo>
<exec command="${build} autoload" dir="." passthru="true"/>
<echo>Building yiilite.php...</echo>
<exec command="${build} lite" dir="." passthru="true"/>
</target>
<target name="message">
<echo>Extracting i18n messages...</echo>
<exec command="${build} message ../framework/messages/config.php" dir="." passthru="true"/>
</target>
<!--
<target name="pear" depends="clean,build">
<echo>Generating pear package for ${phing.project.name}-${yii.release}</echo>
<mkdir dir="${build.dist.dir}" />
<yii-pear pkgdir="${build.pear.src.dir}"
channel="pear.php.net"
version="${yii.release}"
state="stable"
category="framework"
package="${phing.project.name}"
summary="Yii PHP Framework"
pkgdescription="Yii PHP Framework: Best for Web 2.0 Development"
notes="http://www.yiiframework.com/files/CHANGELOG-${yii.release}.txt"
license="BSD"
/>
<exec command="pear package" dir="${build.pear.src.dir}" passthru="true" />
<move file="${build.pear.src.dir}/${pearname}" tofile="${build.dist.dir}/${pearname}" />
</target>
-->
<target name="clean">
<echo>Cleaning up the build...</echo>
<delete dir="${build.base.dir}"/>
</target>
<target name="help">
<echo>
Welcome to use Yii build script!
--------------------------------
You may use the following command format to build a target:
phing &lt;target name&gt;
where &lt;target name&gt; can be one of the following:
- sync : synchronize yiilite.php and YiiBase.php
- message : extract i18n messages of the framework
- src : build source release
- doc : build documentation release (Windows only)
- clean : clean up the build
</echo>
</target>
</project>

112
build/controllers/LocaleController.php

@ -0,0 +1,112 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
use yii\console\Controller;
use yii\helpers\FileHelper;
/**
* http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class LocaleController extends Controller
{
public $defaultAction = 'plural';
/**
* Generates the plural rules data.
*
* This command will parse the plural rule XML file from CLDR and convert them
* into appropriate PHP representation to support Yii message translation feature.
* @param string $xmlFile the original plural rule XML file (from CLDR). This file may be found in
* http://www.unicode.org/Public/cldr/latest/core.zip
* Extract the zip file and locate the file "common/supplemental/plurals.xml".
* @throws Exception
*/
public function actionPlural($xmlFile)
{
if (!is_file($xmlFile)) {
throw new Exception("The source plural rule file does not exist: $xmlFile");
}
$xml = simplexml_load_file($xmlFile);
$allRules = array();
$patterns = array(
'/n in 0..1/' => '(n==0||n==1)',
'/\s+is\s+not\s+/i' => '!=', //is not
'/\s+is\s+/i' => '==', //is
'/n\s+mod\s+(\d+)/i' => 'fmod(n,$1)', //mod (CLDR's "mod" is "fmod()", not "%")
'/^(.*?)\s+not\s+in\s+(\d+)\.\.(\d+)/i' => '!in_array($1,range($2,$3))', //not in
'/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => 'in_array($1,range($2,$3))', //in
'/^(.*?)\s+not\s+within\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not within
'/^(.*?)\s+within\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3)', //within
);
foreach ($xml->plurals->pluralRules as $node) {
$attributes = $node->attributes();
$locales = explode(' ', $attributes['locales']);
$rules = array();
if (!empty($node->pluralRule)) {
foreach ($node->pluralRule as $rule) {
$expr_or = preg_split('/\s+or\s+/i', $rule);
foreach ($expr_or as $key_or => $val_or) {
$expr_and = preg_split('/\s+and\s+/i', $val_or);
$expr_and = preg_replace(array_keys($patterns), array_values($patterns), $expr_and);
$expr_or[$key_or] = implode('&&', $expr_and);
}
$expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or));
$rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) {
if ($matches[2] - $matches[1] <= 5) {
return 'array(' . implode(',', range($matches[1], $matches[2])) . ')';
} else {
return $matches[0];
}
}, $expr);
}
foreach ($locales as $locale) {
$allRules[$locale] = $rules;
}
}
}
// hard fix for "br": the rule is too complex
$allRules['br'] = array(
0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))',
1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))',
2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))',
3 => 'fmod($n,1000000)==0&&$n!=0',
);
if (preg_match('/\d+/', $xml->version['number'], $matches)) {
$revision = $matches[0];
} else {
$revision = -1;
}
echo "<?php\n";
echo <<<EOD
/**
* Plural rules.
*
* This file is automatically generated by the "yiic locale/plural" command under the "build" folder.
* Do not modify it directly.
*
* The original plural rule data used for generating this file has the following copyright terms:
*
* Copyright © 1991-2007 Unicode, Inc. All rights reserved.
* Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
*
* @revision $revision (of the original plural file)
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
EOD;
echo "\nreturn " . var_export($allRules, true) . ';';
}
}

26
docs/api/base/Component.md

@ -1,3 +1,5 @@
Component is the base class that implements the *property*, *event* and *behavior* features.
Component provides the *event* and *behavior* features, in addition to the *property* feature which is implemented in Component provides the *event* and *behavior* features, in addition to the *property* feature which is implemented in
its parent class [[Object]]. its parent class [[Object]].
@ -7,20 +9,20 @@ is triggered (i.e. comment will be added), our custom code will be executed.
An event is identified by a name that should be unique within the class it is defined at. Event names are *case-sensitive*. An event is identified by a name that should be unique within the class it is defined at. Event names are *case-sensitive*.
One or multiple PHP callbacks, called *event handlers*, could be attached to an event. You can call [[trigger()]] to One or multiple PHP callbacks, called *event handlers*, can be attached to an event. You can call [[trigger()]] to
raise an event. When an event is raised, the event handlers will be invoked automatically in the order they were raise an event. When an event is raised, the event handlers will be invoked automatically in the order they were
attached. attached.
To attach an event handler to an event, call [[on()]]: To attach an event handler to an event, call [[on()]]:
~~~ ~~~
$comment->on('add', function($event) { $post->on('update', function($event) {
// send email notification // send email notification
}); });
~~~ ~~~
In the above, we attach an anonymous function to the "add" event of the comment. In the above, an anonymous function is attached to the "update" event of the post. You may attach
Valid event handlers include: the following types of event handlers:
- anonymous function: `function($event) { ... }` - anonymous function: `function($event) { ... }`
- object method: `array($object, 'handleAdd')` - object method: `array($object, 'handleAdd')`
@ -35,8 +37,8 @@ function foo($event)
where `$event` is an [[Event]] object which includes parameters associated with the event. where `$event` is an [[Event]] object which includes parameters associated with the event.
You can also attach an event handler to an event when configuring a component with a configuration array. The syntax is You can also attach a handler to an event when configuring a component with a configuration array.
like the following: The syntax is like the following:
~~~ ~~~
array( array(
@ -46,15 +48,13 @@ array(
where `on add` stands for attaching an event to the `add` event. where `on add` stands for attaching an event to the `add` event.
You can call [[getEventHandlers()]] to retrieve all event handlers that are attached to a specified event. Because this Sometimes, you may want to associate extra data with an event handler when you attach it to an event
method returns a [[Vector]] object, we can manipulate this object to attach/detach event handlers, or adjust their and then access it when the handler is invoked. You may do so by
relative orders.
~~~ ~~~
$handlers = $comment->getEventHandlers('add'); $post->on('update', function($event) {
$handlers->insertAt(0, $callback); // attach a handler as the first one // the data can be accessed via $event->data
$handlers[] = $callback; // attach a handler as the last one }, $data);
unset($handlers[0]); // detach the first handler
~~~ ~~~

30
docs/api/base/Object.md

@ -1,3 +1,5 @@
Object is the base class that implements the *property* feature.
A property is defined by a getter method (e.g. `getLabel`), and/or a setter method (e.g. `setLabel`). For example, A property is defined by a getter method (e.g. `getLabel`), and/or a setter method (e.g. `setLabel`). For example,
the following getter and setter methods define a property named `label`: the following getter and setter methods define a property named `label`:
@ -30,4 +32,30 @@ $object->label = 'abc';
If a property has only a getter method and has no setter method, it is considered as *read-only*. In this case, trying If a property has only a getter method and has no setter method, it is considered as *read-only*. In this case, trying
to modify the property value will cause an exception. to modify the property value will cause an exception.
One can call [[hasProperty]], [[canGetProperty]] and/or [[canSetProperty]] to check the existence of a property. One can call [[hasProperty()]], [[canGetProperty()]] and/or [[canSetProperty()]] to check the existence of a property.
Besides the property feature, Object also introduces an important object initialization life cycle. In particular,
creating an new instance of Object or its derived class will involve the following life cycles sequentially:
1. the class constructor is invoked;
2. object properties are initialized according to the given configuration;
3. the `init()` method is invoked.
In the above, both Step 2 and 3 occur at the end of the class constructor. It is recommended that
you perform object initialization in the `init()` method because at that stage, the object configuration
is already applied.
In order to ensure the above life cycles, if a child class of Object needs to override the constructor,
it should be done like the following:
~~~
public function __construct($param1, $param2, ..., $config = array())
{
...
parent::__construct($config);
}
~~~
That is, a `$config` parameter (defaults to `array()`) should be declared as the last parameter
of the constructor, and the parent implementation should be called at the end of the constructor.

328
docs/code_style.md

@ -1,328 +0,0 @@
Yii2 code standard
==================
This code standard is used for all the Yii2 core classes and can be applied to
your application in order to achieve consistency among your team. Also it will
help in case you want to opensource code.
PHP file formatting
-------------------
### General
- Do not end file with `?>` if it contains PHP code only.
- Do not use `<?`. Use `<?php` instead.
- Files should be encoded in UTF-8.
- Any file that contains PHP code should end with the extension `.php`.
- Do not add trailing spaces to the end of the lines.
#### Indentation
All code must be indented with tabs. That includes both PHP and JavaScript code.
#### Maximum Line Length
We're not strictly limiting maximum line length but sticking to 80 characters
where possible.
### PHP types
All PHP types and values should be used lowercase. That includes `true`, `false`,
`null` and `array`.
### Strings
- If string doesn't contain variables or single quotes, use single quotes.
~~~
$str = 'Like this.';
~~~
- If string contains single quotes you can use double quotes to avoid extra escaping.
- You can use the following forms of variable substitution:
~~~
$str1 = "Hello $username!";
$str2 = "Hello {$username}!";
~~~
The following is not permitted:
~~~
$str3 = "Hello ${username}!";
~~~
### String concatenation
Add spaces around dot when concatenating strings:
~~~
$name = 'Yii' . ' Framework';
~~~
When string is long format is the following:
~~~
$sql = "SELECT *"
. "FROM `post` "
. "WHERE `id` = 121 ";
~~~
### Numerically indexed arrays
- Do not use negative numbers as array indexes.
Use the following formatting when declaring array:
~~~
$arr = array(3, 14, 15, 'Yii', 'Framework');
~~~
If there are too many elements for a single line:
~~~
$arr = array(
3, 14, 15,
92, 6, $test,
'Yii', 'Framework',
);
~~~
### Associative arrays
Use the following format for associative arrays:
~~~
$config = array(
'name' => 'Yii',
'options' => array(
'usePHP' => true,
),
);
~~~
### Classes
- Classes should be named using `CamelCase`.
- The brace should always be written on the line underneath the class name.
- Every class must have a documentation block that conforms to the PHPDoc.
- All code in a class must be indented with a single tab.
- There should be only one class in a single PHP file.
- All classes should be namespaced.
- Class name should match file name. Class namespace should match directory structure.
~~~
/**
* Documentation
*/
class MyClass extends \yii\Object implements MyInterface
{
// code
}
~~~
### Class members and variables
- When declaring public class members specify `public` keyword explicitly.
- Variables should be declared at the top of the class before any method declarations.
- Private and protected variables should be named like `$_varName`.
- Public class members and standalone variables should be named using `$camelCase`
with first letter lowercase.
- Use descriptive names. Variables such as `$i` and `$j` are better not to be used.
### Constants
Both class level constants and global constants should be named in uppercase. Words
are separated by underscore.
~~~
class User {
const STATUS_ACTIVE = 1;
const STATUS_BANNED = 2;
}
~~~
It's preferable to define class level constants rather than global ones.
### Functions and methods
- Functions and methods should be named using `camelCase` with first letter lowercase.
- Name should be descriptive by itself indicating the purpose of the function.
- Class methods should always declare visibility using `private`, `protected` and
`public` modifiers. `var` is not allowed.
- Opening brace of a function should be on the line after the function declaration.
~~~
/**
* Documentation
*/
class Foo
{
/**
* Documentation
*/
public function bar()
{
// code
return $value;
}
}
~~~
Use type hinting where possible:
~~~
public function __construct(CDbConnection $connection)
{
$this->connection = $connection;
}
~~~
### Function and method calls
~~~
doIt(2, 3);
doIt(array(
'a' => 'b',
));
doIt('a', array(
'a' => 'b',
));
~~~
### Control statements
- Control statement condition must have single space before and after parenthesis.
- Operators inside of parenthesis should be separated by spaces.
- Opening brace is on the same line.
- Closing brace is on a new line.
- Always use braces for single line statements.
~~~
if ($event === null) {
return new Event();
} elseif ($event instanceof CoolEvent) {
return $event->instance();
} else {
return null;
}
// the following is NOT allowed:
if(!$model)
throw new Exception('test');
~~~
### Switch
Use the following formatting for switch:
~~~
switch ($this->phpType) {
case 'string':
$a = (string)$value;
break;
case 'integer':
case 'int':
$a = (integer)$value;
break;
case 'boolean':
$a = (boolean)$value;
break;
default:
$a = null;
}
~~~
### Code documentation
- Refer ot [phpDoc](http://phpdoc.org/) for documentation syntax.
- Code without documentation is not allowed.
- All class files must contain a "file-level" docblock at the top of each file
and a "class-level" docblock immediately above each class.
- There is no need to use `@return` if method does return nothing.
#### File
~~~
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
~~~
#### Class
~~~
/**
* Component is the base class that provides the *property*, *event* and *behavior* features.
*
* @include @yii/docs/base-Component.md
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Component extends \yii\base\Object
~~~
#### Function / method
~~~
/**
* Returns the list of attached event handlers for an event.
* You may manipulate the returned [[Vector]] object by adding or removing handlers.
* For example,
*
* ~~~
* $component->getEventHandlers($eventName)->insertAt(0, $eventHandler);
* ~~~
*
* @param string $name the event name
* @return Vector list of attached event handlers for the event
* @throws Exception if the event is not defined
*/
public function getEventHandlers($name)
{
if (!isset($this->_e[$name])) {
$this->_e[$name] = new Vector;
}
$this->ensureBehaviors();
return $this->_e[$name];
}
~~~
#### Comments
- One-line comments should be started with `//` and not `#`.
- One-line comment should be on its own line.
Yii application naming conventions
----------------------------------
Other library and framework standards
-------------------------------------
It's good to be consistent with other frameworks and libraries whose components
will be possibly used with Yii2. That's why when there are no objective reasons
to use different style we should use one that's common among most of the popular
libraries and frameworks.
That's not only about PHP but about JavaScript as well. Since we're using jQuery
a lot it's better to be consistent with its style as well.
Application style consistency is much more important than consistency with other frameworks and libraries.
- [Symfony 2](http://symfony.com/doc/current/contributing/code/standards.html)
- [Zend Framework 1](http://framework.zend.com/manual/en/coding-standard.coding-style.html)
- [Zend Framework 2](http://framework.zend.com/wiki/display/ZFDEV2/Coding+Standards)
- [Pear](http://pear.php.net/manual/en/standards.php)
- [jQuery](http://docs.jquery.com/JQuery_Core_Style_Guidelines)

BIN
docs/full_2011_11_12.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

BIN
docs/hierarchy_2011_11_12.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

192
docs/review_2011_11_12_alex.md

@ -1,192 +0,0 @@
Alex's Code Review, 2011.11.12
==============================
Overall hierarchy
------------------
Generally is OK. Like that `Object` and `Component` are now separated.
I've generated 2 diagrams under `docs/` to see it better as a whole.
> The purpose of separating `Object` from `Component` is to make `Object`
> a super-light base class that supports properties defined by getter/setters.
> Note that `Component` is a bit of heavy because it uses two extra member
> variables to support events and behaviors.
Object
------
### property feature
Is it OK that `canGetProperty` and `canSetProperty` will return `false` for real
class members?
> Added $checkVar parameter
### callbacks and expressions
We're using 5.3. What's the reason to support `eval()` in `evaluateExpression` if
we have anonymous functions? Is that for storing code as string inside of DB (RBAC)?
If we're going to get rid of `eval()`, cosider remaning method to something about callback.
If not then we definitely need to use anonymous functions in API docs and the guide
where possible.
> The purpose of evaluateExpression() is to provide a way of evaluating a PHP expression
> in the context of an object. Will remove it before release if we find no use of it.
>> mdomba:
>> As eval() is controversial, and anonymous functions can replace all Yii 1 usage of eval()
>> how about removing it from the beginning and add it only if we find it necessary.
>> This way we would not be tempted to stick with eval() and will be forced to first try to find alternatives
### Object::create()
#### `__construct` issue
Often a class doesn't have `__construct` implementation and `stdClass` doesn't have
default one either but Object::create() always expects constructor to be
defined. See `ObjectTest`. Either `method_exists` call or `Object::__construct` needed.
> Added Object::__construct.
#### How to support object factory like we do with CWidgetFactory?
~~~
class ObjectConfig
{
public function configure($class)
{
$config = $this->load($class);
// apply config to $class
}
private function load($class)
{
// get class properties from a config file
// in this method we need to walk all the
// inheritance hierarchy down to Object itself
return array(
'property' => 'value',
// …
);
}
}
~~~
Then we need to add `__construct` to `Object` (or implement `Initalbe`):
~~~
class Object
{
public function __construct()
{
$conf = new ObjectConfig();
$conf->configure($this);
}
}
~~~
This way we'll be able to set defaults for any object.
> The key issue here is about how to process the config file. Clearly, we cannot
> do this for every type of component because it would mean an extra file access
> for every component type
#### Do we need to support lazy class injection?
Currently there's no way to lazy-inject class into another class property via
config. Do we need it? If yes then we can probably extend component config to support
the following:
~~~
class Foo extends Object
{
public $prop;
}
class Bar extends Object
{
public $prop;
}
$config = array(
'prop' => array(
'class' => 'Bar',
'prop' => 'Hello!',
),
);
$foo = Foo::create($config);
echo $foo->bar->prop;
// will output Hello!
~~~
Should it support infinite nesting level?
> I don't think we need this. Foo::$prop cannot be an object unless it needs it to be.
> In that case, it can be defined with a setter in which it can handle the object creation
> based on a configuration array. This is a bit inconvenient, but I think such usage is
> not very common.
### Why `Event` is `Object`?
There's no need to extend from `Object`. Is there a plan to use `Object` features
later?
> To use properties defined via getter/setter.
Behaviors
---------
Overall I wasn't able to use behaviors. See `BehaviorTest`.
### Should behaviors be able to define events for owner components?
Why not? Should be a very good feature in order to make behaviors customizable.
> It's a bit hard to implement it efficiently. I tend not to support it for now
> unless enough people are requesting for it.
### Multiple behaviors can be attached to the same component
What if we'll have multiple methods / properties / events with the same name?
> The first one takes precedence. This is the same as we do in 1.1.
### How to use Behavior::attach?
Looks like it is used by `Component::attachBehavior` but can't be used without it.
Why it's public then? Can we move it to `Component?`
> It's public because it is called by Component. It is in Behavior such that
> it can be overridden by behavior classes to customize the attach process.
Events
------
Class itself looks OK. Component part is OK as well but I've not tested
it carefully. Overall it seems concept is the same as in Yii1.
### Event declaration: the on-method is mostly repetitive for every event. Should we choose a different way of declaring events?
Maybe. People complained previously about too many code for event declaration.
### Should we implement some additional event mechanism, such as global events?
Why use two different implementations in a single application?
Exceptions
----------
- Should we convert all errors, warnings and notices to exceptions?
> I think not. We used to do this in early versions of 1.0. We found sometimes
> very mysterious things would happen which makes error fixing harder rather than
> easier.
Coding style
------------
See `docs/code_style.md`.

379
framework/YiiBase.php

@ -7,6 +7,7 @@
use yii\base\Exception; use yii\base\Exception;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\base\InvalidParamException; use yii\base\InvalidParamException;
use yii\base\UnknownClassException;
use yii\logging\Logger; use yii\logging\Logger;
/** /**
@ -46,20 +47,18 @@ class YiiBase
{ {
/** /**
* @var array class map used by the Yii autoloading mechanism. * @var array class map used by the Yii autoloading mechanism.
* The array keys are the class names, and the array values are the corresponding class file paths. * The array keys are the class names (without leading backslashes), and the array values
* This property mainly affects how [[autoload]] works. * are the corresponding class file paths (or path aliases). This property mainly affects
* how [[autoload()]] works.
* @see import * @see import
* @see autoload * @see autoload
*/ */
public static $classMap = array(); public static $classMap = array();
/** /**
* @var array list of directories where Yii will search for new classes to be included. * @var boolean whether to search PHP include_path when autoloading unknown classes.
* The first directory in the array will be searched first, and so on. * You may want to turn this off if you are also using autoloaders from other libraries.
* This property mainly affects how [[autoload]] works.
* @see import
* @see autoload
*/ */
public static $classPath = array(); public static $enableIncludePath = true;
/** /**
* @var yii\console\Application|yii\web\Application the application instance * @var yii\console\Application|yii\web\Application the application instance
*/ */
@ -106,108 +105,115 @@ class YiiBase
} }
/** /**
* Imports a class or a directory. * Imports a class by its alias.
* *
* Importing a class is like including the corresponding class file. * This method is provided to support autoloading of non-namespaced classes.
* The main difference is that importing a class is much lighter because it only * Such a class can be specified in terms of an alias. For example, the alias `@old/code/Sample`
* includes the class file when the class is referenced in the code the first time. * may represent the `Sample` class under the directory `@old/code` (a path alias).
* *
* Importing a directory will add the directory to the front of the [[classPath]] array. * By importing a class, the class is put in an internal storage such that when
* When [[autoload]] is loading an unknown class, it will search in the directories * the class is used for the first time, the class autoloader will be able to
* specified in [[classPath]] to find the corresponding class file to include. * find the corresponding class file and include it. For this reason, this method
* For this reason, if multiple directories are imported, the directories imported later * is much lighter than `include()`.
* will take precedence in class file searching. *
* * You may import the same class multiple times. Only the first importing will count.
* The same class or directory can be imported multiple times. Only the first importing *
* will count. Importing a directory does not import any of its subdirectories. * @param string $alias the class to be imported. This may be either a class alias or a fully-qualified class name.
* * If the latter, it will be returned back without change.
* To import a class or a directory, one can use either path alias or class name (can be namespaced): * @return string the actual class name that `$alias` refers to
* * @throws Exception if the alias is invalid
* - `@app/components/GoogleMap`: importing the `GoogleMap` class with a path alias;
* - `@app/components/*`: importing the whole `components` directory with a path alias;
* - `GoogleMap`: importing the `GoogleMap` class with a class name. [[autoload()]] will be used
* when this class is used for the first time.
*
* @param string $alias path alias or a simple class name to be imported
* @param boolean $forceInclude whether to include the class file immediately. If false, the class file
* will be included only when the class is being used. This parameter is used only when
* the path alias refers to a class.
* @return string the class name or the directory that this alias refers to
* @throws Exception if the path alias is invalid
*/ */
public static function import($alias, $forceInclude = false) public static function import($alias)
{ {
if (isset(self::$_imported[$alias])) { if (strncmp($alias, '@', 1)) {
return self::$_imported[$alias];
}
if ($alias[0] !== '@') {
// a simple class name
if (class_exists($alias, false) || interface_exists($alias, false)) {
return self::$_imported[$alias] = $alias;
}
if ($forceInclude && static::autoload($alias)) {
self::$_imported[$alias] = $alias;
}
return $alias; return $alias;
} } else {
$alias = static::getAlias($alias);
if (!isset(self::$_imported[$alias])) {
$className = basename($alias); $className = basename($alias);
$isClass = $className !== '*'; self::$_imported[$alias] = $className;
self::$classMap[$className] = $alias . '.php';
if ($isClass && (class_exists($className, false) || interface_exists($className, false))) { }
return self::$_imported[$alias] = $className; return self::$_imported[$alias];
}
} }
$path = static::getAlias(dirname($alias)); /**
* Imports a set of namespaces.
if ($isClass) { *
if ($forceInclude) { * By importing a namespace, the method will create an alias for the directory corresponding
require($path . "/$className.php"); * to the namespace. For example, if "foo\bar" is a namespace associated with the directory
self::$_imported[$alias] = $className; * "path/to/foo/bar", then an alias "@foo/bar" will be created for this directory.
} else { *
self::$classMap[$className] = $path . DIRECTORY_SEPARATOR . "$className.php"; * This method is typically invoked in the bootstrap file to import the namespaces of
* the installed extensions. By default, Composer, when installing new extensions, will
* generate such a mapping file which can be loaded and passed to this method.
*
* @param array $namespaces the namespaces to be imported. The keys are the namespaces,
* and the values are the corresponding directories.
*/
public static function importNamespaces($namespaces)
{
foreach ($namespaces as $name => $path) {
if ($name !== '') {
$name = '@' . str_replace('\\', '/', $name);
static::setAlias($name, $path);
} }
return $className;
} else {
// a directory
array_unshift(self::$classPath, $path);
return self::$_imported[$alias] = $path;
} }
} }
/** /**
* Translates a path alias into an actual path. * Translates a path alias into an actual path.
* *
* The path alias can be either a root alias registered via [[setAlias]] or an * The translation is done according to the following procedure:
* alias starting with a root alias (e.g. `@yii/base/Component.php`). *
* In the latter case, the root alias will be replaced by the corresponding registered path * 1. If the given alias does not start with '@', it is returned back without change;
* and the remaining part will be appended to it. * 2. Otherwise, look for the longest registered alias that matches the beginning part
* of the given alias. If it exists, replace the matching part of the given alias with
* the corresponding registered path.
* 3. Throw an exception or return false, depending on the `$throwException` parameter.
*
* For example, by default '@yii' is registered as the alias to the Yii framework directory,
* say '/path/to/yii'. The alias '@yii/web' would then be translated into '/path/to/yii/web'.
* *
* In case the given parameter is not an alias (i.e., not starting with '@'), * If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config'
* it will be returned back without change. * would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path.
* This is because the longest alias takes precedence.
* *
* Note, this method does not ensure the existence of the resulting path. * However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced
* @param string $alias alias * instead of '@foo/bar', because '/' serves as the boundary character.
*
* Note, this method does not check if the returned path exists or not.
*
* @param string $alias the alias to be translated.
* @param boolean $throwException whether to throw an exception if the given alias is invalid. * @param boolean $throwException whether to throw an exception if the given alias is invalid.
* If this is false and an invalid alias is given, false will be returned by this method. * If this is false and an invalid alias is given, false will be returned by this method.
* @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. * @return string|boolean the path corresponding to the alias, false if the root alias is not previously registered.
* @throws InvalidParamException if the alias is invalid while $throwException is true.
* @see setAlias * @see setAlias
*/ */
public static function getAlias($alias, $throwException = true) public static function getAlias($alias, $throwException = true)
{ {
if (is_string($alias)) { if (strncmp($alias, '@', 1)) {
if (isset(self::$aliases[$alias])) { // not an alias
return self::$aliases[$alias];
} elseif ($alias === '' || $alias[0] !== '@') { // not an alias
return $alias; return $alias;
} elseif (($pos = strpos($alias, '/')) !== false || ($pos = strpos($alias, '\\')) !== false) { }
$rootAlias = substr($alias, 0, $pos);
if (isset(self::$aliases[$rootAlias])) { $pos = strpos($alias, '/');
return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); $root = $pos === false ? $alias : substr($alias, 0, $pos);
if (isset(self::$aliases[$root])) {
if (is_string(self::$aliases[$root])) {
return $pos === false ? self::$aliases[$root] : self::$aliases[$root] . substr($alias, $pos);
} else {
foreach (self::$aliases[$root] as $name => $path) {
if (strpos($alias . '/', $name . '/') === 0) {
return $path . substr($alias, strlen($name));
}
} }
} }
} }
if ($throwException) { if ($throwException) {
throw new InvalidParamException("Invalid path alias: $alias"); throw new InvalidParamException("Invalid path alias: $alias");
} else { } else {
@ -216,39 +222,96 @@ class YiiBase
} }
/** /**
* Returns the root alias part of a given alias.
* A root alias is an alias that has been registered via [[setAlias()]] previously.
* If a given alias matches multiple root aliases, the longest one will be returned.
* @param string $alias the alias
* @return string|boolean the root alias, or false if no root alias is found
*/
public static function getRootAlias($alias)
{
$pos = strpos($alias, '/');
$root = $pos === false ? $alias : substr($alias, 0, $pos);
if (isset(self::$aliases[$root])) {
if (is_string(self::$aliases[$root])) {
return $root;
} else {
foreach (self::$aliases[$root] as $name => $path) {
if (strpos($alias . '/', $name . '/') === 0) {
return $name;
}
}
}
}
return false;
}
/**
* Registers a path alias. * Registers a path alias.
* *
* A path alias is a short name representing a path (a file path, a URL, etc.) * A path alias is a short name representing a long path (a file path, a URL, etc.)
* A path alias must start with '@' (e.g. '@yii'). * For example, we use '@yii' as the alias of the path to the Yii framework directory.
* *
* Note that this method neither checks the existence of the path nor normalizes the path. * A path alias must start with the character '@' so that it can be easily differentiated
* Any trailing '/' and '\' characters in the path will be trimmed. * from non-alias paths.
* *
* @param string $alias alias to the path. The alias must start with '@'. * Note that this method does not check if the given path exists or not. All it does is
* @param string $path the path corresponding to the alias. This can be * to associate the alias with the path.
*
* Any trailing '/' and '\' characters in the given path will be trimmed.
*
* @param string $alias the alias name (e.g. "@yii"). It must start with a '@' character.
* It may contain the forward slash '/' which serves as boundary character when performing
* alias translation by [[getAlias()]].
* @param string $path the path corresponding to the alias. Trailing '/' and '\' characters
* will be trimmed. This can be
* *
* - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`) * - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`)
* - a URL (e.g. `http://www.yiiframework.com`) * - a URL (e.g. `http://www.yiiframework.com`)
* - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the
* actual path first by calling [[getAlias]]. * actual path first by calling [[getAlias()]].
* @throws Exception if $path is an invalid alias *
* @throws InvalidParamException if $path is an invalid alias.
* @see getAlias * @see getAlias
*/ */
public static function setAlias($alias, $path) public static function setAlias($alias, $path)
{ {
if ($path === null) { if (strncmp($alias, '@', 1)) {
unset(self::$aliases[$alias]); $alias = '@' . $alias;
} elseif ($path[0] !== '@') { }
self::$aliases[$alias] = rtrim($path, '\\/'); $pos = strpos($alias, '/');
$root = $pos === false ? $alias : substr($alias, 0, $pos);
if ($path !== null) {
$path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path);
if (!isset(self::$aliases[$root])) {
self::$aliases[$root] = $path;
} elseif (is_string(self::$aliases[$root])) {
if ($pos === false) {
self::$aliases[$root] = $path;
} else {
self::$aliases[$root] = array(
$alias => $path,
$root => self::$aliases[$root],
);
}
} else { } else {
self::$aliases[$alias] = static::getAlias($path); self::$aliases[$root][$alias] = $path;
krsort(self::$aliases[$root]);
}
} elseif (isset(self::$aliases[$root])) {
if (is_array(self::$aliases[$root])) {
unset(self::$aliases[$root][$alias]);
} elseif ($pos === false) {
unset(self::$aliases[$root]);
}
} }
} }
/** /**
* Class autoload loader. * Class autoload loader.
* This method is invoked automatically when the execution encounters an unknown class. * This method is invoked automatically when PHP sees an unknown class.
* The method will attempt to include the class file as follows: * The method will attempt to include the class file according to the following procedure:
* *
* 1. Search in [[classMap]]; * 1. Search in [[classMap]];
* 2. If the class is namespaced (e.g. `yii\base\Component`), it will attempt * 2. If the class is namespaced (e.g. `yii\base\Component`), it will attempt
@ -257,73 +320,75 @@ class YiiBase
* 3. If the class is named in PEAR style (e.g. `PHPUnit_Framework_TestCase`), * 3. If the class is named in PEAR style (e.g. `PHPUnit_Framework_TestCase`),
* it will attempt to include the file associated with the corresponding path alias * it will attempt to include the file associated with the corresponding path alias
* (e.g. `@PHPUnit/Framework/TestCase.php`); * (e.g. `@PHPUnit/Framework/TestCase.php`);
* 4. Search in [[classPath]]; * 4. Search PHP include_path for the actual class file if [[enableIncludePath]] is true;
* 5. Return false so that other autoloaders have chance to include the class file. * 5. Return false so that other autoloaders have chance to include the class file.
* *
* @param string $className class name * @param string $className class name
* @return boolean whether the class has been loaded successfully * @return boolean whether the class has been loaded successfully
* @throws Exception if the class file does not exist * @throws InvalidConfigException if the class file does not exist
* @throws UnknownClassException if the class does not exist in the class file
*/ */
public static function autoload($className) public static function autoload($className)
{ {
$className = ltrim($className, '\\');
if (isset(self::$classMap[$className])) { if (isset(self::$classMap[$className])) {
include(self::$classMap[$className]); $classFile = static::getAlias(self::$classMap[$className]);
return true; if (!is_file($classFile)) {
throw new InvalidConfigException("Class file does not exist: $classFile");
} }
} else {
if (strpos($className, '\\') !== false) { // follow PSR-0 to determine the class file
if (($pos = strrpos($className, '\\')) !== false) {
// namespaced class, e.g. yii\base\Component // namespaced class, e.g. yii\base\Component
// convert namespace to path alias, e.g. yii\base\Component to @yii/base/Component $path = str_replace('\\', '/', substr($className, 0, $pos + 1))
$alias = '@' . str_replace('\\', '/', ltrim($className, '\\')); . str_replace('_', '/', substr($className, $pos + 1)) . '.php';
if (($path = static::getAlias($alias, false)) !== false) { } else {
$classFile = $path . '.php'; $path = str_replace('_', '/', $className) . '.php';
} }
} elseif (($pos = strpos($className, '_')) !== false) {
// PEAR-styled class, e.g. PHPUnit_Framework_TestCase // try via path alias first
// convert class name to path alias, e.g. PHPUnit_Framework_TestCase to @PHPUnit/Framework/TestCase if (strpos($path, '/') !== false) {
$alias = '@' . str_replace('_', '/', $className); $fullPath = static::getAlias('@' . $path, false);
if (($path = static::getAlias($alias, false)) !== false) { if ($fullPath !== false && is_file($fullPath)) {
$classFile = $path . '.php'; $classFile = $fullPath;
} }
} }
if (!isset($classFile)) { // search include_path
// search in include paths if (!isset($classFile) && self::$enableIncludePath && ($fullPath = stream_resolve_include_path($path)) !== false) {
foreach (self::$classPath as $path) { $classFile = $fullPath;
$path .= DIRECTORY_SEPARATOR . $className . '.php';
if (is_file($path)) {
$classFile = $path;
$alias = $className;
} }
if (!isset($classFile)) {
// return false to let other autoloaders to try loading the class
return false;
} }
} }
if (isset($classFile, $alias) && is_file($classFile)) {
if (!YII_DEBUG || basename(realpath($classFile)) === basename($alias) . '.php') {
include($classFile); include($classFile);
if (class_exists($className, false) || interface_exists($className, false)) {
return true; return true;
} else { } else {
throw new Exception("Class name '$className' does not match the class file '" . realpath($classFile) . "'. Have you checked their case sensitivity?"); throw new UnknownClassException("Unable to find '$className' in file: $classFile");
}
} }
return false;
} }
/** /**
* Creates a new object using the given configuration. * Creates a new object using the given configuration.
* *
* The configuration can be either a string or an array. * The configuration can be either a string or an array.
* If a string, it is treated as the *object type*; if an array, * If a string, it is treated as the *object class*; if an array,
* it must contain a `class` element specifying the *object type*, and * it must contain a `class` element specifying the *object class*, and
* the rest of the name-value pairs in the array will be used to initialize * the rest of the name-value pairs in the array will be used to initialize
* the corresponding object properties. * the corresponding object properties.
* *
* The object type can be either a class name or the [[getAlias|alias]] of * The object type can be either a class name or the [[getAlias()|alias]] of
* the class. For example, * the class. For example,
* *
* - `\app\components\GoogleMap`: fully-qualified namespaced class. * - `app\components\GoogleMap`: fully-qualified namespaced class.
* - `@app/components/GoogleMap`: an alias * - `@app/components/GoogleMap`: an alias, used for non-namespaced class.
* *
* Below are some usage examples: * Below are some usage examples:
* *
@ -366,7 +431,13 @@ class YiiBase
} }
if (!class_exists($class, false)) { if (!class_exists($class, false)) {
$class = static::import($class, true); $class = static::import($class);
}
$class = ltrim($class, '\\');
if (isset(self::$objectConfig[$class])) {
$config = array_merge(self::$objectConfig[$class], $config);
} }
if (($n = func_num_args()) > 1) { if (($n = func_num_args()) > 1) {
@ -504,23 +575,31 @@ class YiiBase
/** /**
* Translates a message to the specified language. * Translates a message to the specified language.
* This method supports choice format (see {@link CChoiceFormat}), *
* i.e., the message returned will be chosen from a few candidates according to the given * The translation will be conducted according to the message category and the target language.
* number value. This feature is mainly used to solve plural format issue in case * To specify the category of the message, prefix the message with the category name and separate it
* a message has different plural forms in some languages. * with "|". For example, "app|hello world". If the category is not specified, the default category "app"
* @param string $message the original message * will be used. The actual message translation is done by a [[\yii\i18n\MessageSource|message source]].
* @param array $params parameters to be applied to the message using <code>strtr</code>. *
* The first parameter can be a number without key. * In case when a translated message has different plural forms (separated by "|"), this method
* And in this case, the method will call {@link CChoiceFormat::format} to choose * will also attempt to choose an appropriate one according to a given numeric value which is
* an appropriate message translation. * specified as the first parameter (indexed by 0) in `$params`.
* You can pass parameter for {@link CChoiceFormat::format} *
* or plural forms format without wrapping it with array. * For example, if a translated message is "I have an apple.|I have {n} apples.", and the first
* @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. * parameter is 2, the message returned will be "I have 2 apples.". Note that the placeholder "{n}"
* @return string the translated message * will be replaced with the given number.
* @see CMessageSource *
* For more details on how plural rules are applied, please refer to:
* [[http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html]]
*
* @param string $message the message to be translated.
* @param array $params the parameters that will be used to replace the corresponding placeholders in the message.
* @param string $language the language code (e.g. `en_US`, `en`). If this is null, the current
* [[\yii\base\Application::language|application language]] will be used.
* @return string the translated message.
*/ */
public static function t($message, $params = array(), $language = null) public static function t($message, $params = array(), $language = null)
{ {
return Yii::$app->getI18N()->translate($message, $params, $language); return self::$app->getI18N()->translate($message, $params, $language);
} }
} }

5
framework/assets.php

@ -0,0 +1,5 @@
<?php
return array(
'basePath' => __DIR__ . '/web/assets',
);

4
framework/base/ActionEvent.php

@ -22,7 +22,9 @@ class ActionEvent extends Event
*/ */
public $action; public $action;
/** /**
* @var boolean whether to continue running the action. * @var boolean whether to continue running the action. Event handlers of
* [[Controller::EVENT_BEFORE_ACTION]] may set this property to decide whether
* to continue running the current action.
*/ */
public $isValid = true; public $isValid = true;

269
framework/base/Application.php

@ -13,36 +13,6 @@ use yii\helpers\FileHelper;
/** /**
* Application is the base class for all application classes. * Application is the base class for all application classes.
* *
* An application serves as the global context that the user request
* is being processed. It manages a set of application components that
* provide specific functionalities to the whole application.
*
* The core application components provided by Application are the following:
* <ul>
* <li>{@link getErrorHandler errorHandler}: handles PHP errors and
* uncaught exceptions. This application component is dynamically loaded when needed.</li>
* <li>{@link getSecurityManager securityManager}: provides security-related
* services, such as hashing, encryption. This application component is dynamically
* loaded when needed.</li>
* <li>{@link getStatePersister statePersister}: provides global state
* persistence method. This application component is dynamically loaded when needed.</li>
* <li>{@link getCache cache}: provides caching feature. This application component is
* disabled by default.</li>
* </ul>
*
* Application will undergo the following life cycles when processing a user request:
* <ol>
* <li>load application configuration;</li>
* <li>set up class autoloader and error handling;</li>
* <li>load static application components;</li>
* <li>{@link beforeRequest}: preprocess the user request; `beforeRequest` event raised.</li>
* <li>{@link processRequest}: process the user request;</li>
* <li>{@link afterRequest}: postprocess the user request; `afterRequest` event raised.</li>
* </ol>
*
* Starting from lifecycle 3, if a PHP error or an uncaught exception occurs,
* the application will switch to its error handling logic and jump to step 6 afterwards.
*
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
@ -87,50 +57,51 @@ class Application extends Module
*/ */
public $layout = 'main'; public $layout = 'main';
// todo
public $localeDataPath = '@yii/i18n/data';
private $_runtimePath;
private $_ended = false; private $_ended = false;
/** /**
* @var string Used to reserve memory for fatal error handler. This memory * @var string Used to reserve memory for fatal error handler.
* reserve can be removed if it's OK to write to PHP log only in this particular case.
*/ */
private $_memoryReserve; private $_memoryReserve;
/** /**
* Constructor. * Constructor.
* @param string $id the ID of this application. The ID should uniquely identify the application from others. * @param array $config name-value pairs that will be used to initialize the object properties.
* @param string $basePath the base path of this application. This should point to * Note that the configuration must contain both [[id]] and [[basePath]].
* the directory containing all application logic, template and data. * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing.
* @param array $config name-value pairs that will be used to initialize the object properties
*/ */
public function __construct($id, $basePath, $config = array()) public function __construct($config = array())
{ {
Yii::$app = $this; Yii::$app = $this;
$this->id = $id;
$this->setBasePath($basePath);
if (YII_ENABLE_ERROR_HANDLER) { if (!isset($config['id'])) {
ini_set('display_errors', 0); throw new InvalidConfigException('The "id" configuration is required.');
set_exception_handler(array($this, 'handleException'));
set_error_handler(array($this, 'handleError'), error_reporting());
} }
$this->registerDefaultAliases(); if (isset($config['basePath'])) {
$this->setBasePath($config['basePath']);
Yii::setAlias('@app', $this->getBasePath());
unset($config['basePath']);
} else {
throw new InvalidConfigException('The "basePath" configuration is required.');
}
$this->registerErrorHandlers();
$this->registerCoreComponents(); $this->registerCoreComponents();
Component::__construct($config); Component::__construct($config);
} }
/** /**
* Initializes the application by loading components declared in [[preload]]. * Registers error handlers.
* If you override this method, make sure the parent implementation is invoked.
*/ */
public function init() public function registerErrorHandlers()
{ {
$this->preloadComponents(); if (YII_ENABLE_ERROR_HANDLER) {
ini_set('display_errors', 0);
set_exception_handler(array($this, 'handleException'));
set_error_handler(array($this, 'handleError'), error_reporting());
}
} }
/** /**
@ -155,55 +126,6 @@ class Application extends Module
} }
/** /**
* Handles fatal PHP errors
*/
public function handleFatalError()
{
if (YII_ENABLE_ERROR_HANDLER) {
$error = error_get_last();
if (ErrorException::isFatalErorr($error)) {
unset($this->_memoryReserve);
$exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
if (function_exists('xdebug_get_function_stack')) {
$trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1);
foreach ($trace as &$frame) {
if (!isset($frame['function'])) {
$frame['function'] = 'unknown';
}
// XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695
if (!isset($frame['type'])) {
$frame['type'] = '::';
}
// XDebug has a different key name
$frame['args'] = array();
if (isset($frame['params']) && !isset($frame['args'])) {
$frame['args'] = $frame['params'];
}
}
$ref = new \ReflectionProperty('Exception', 'trace');
$ref->setAccessible(true);
$ref->setValue($exception, $trace);
}
$this->logException($exception);
if (($handler = $this->getErrorHandler()) !== null) {
@$handler->handle($exception);
} else {
$this->renderException($exception);
}
exit(1);
}
}
}
/**
* Runs the application. * Runs the application.
* This is the main entrance of an application. * This is the main entrance of an application.
* @return integer the exit status (0 means normal, non-zero values mean abnormal) * @return integer the exit status (0 means normal, non-zero values mean abnormal)
@ -246,6 +168,8 @@ class Application extends Module
return 0; return 0;
} }
private $_runtimePath;
/** /**
* Returns the directory that stores runtime files. * Returns the directory that stores runtime files.
* @return string the directory that stores runtime files. Defaults to 'protected/runtime'. * @return string the directory that stores runtime files. Defaults to 'protected/runtime'.
@ -265,14 +189,37 @@ class Application extends Module
*/ */
public function setRuntimePath($path) public function setRuntimePath($path)
{ {
$p = FileHelper::ensureDirectory($path); $path = Yii::getAlias($path);
if (is_writable($p)) { if (is_dir($path) && is_writable($path)) {
$this->_runtimePath = $p; $this->_runtimePath = $path;
} else { } else {
throw new InvalidConfigException("Runtime path must be writable by the Web server process: $path"); throw new InvalidConfigException("Runtime path must be a directory writable by the Web server process: $path");
} }
} }
private $_vendorPath;
/**
* Returns the directory that stores vendor files.
* @return string the directory that stores vendor files. Defaults to 'protected/vendor'.
*/
public function getVendorPath()
{
if ($this->_vendorPath === null) {
$this->setVendorPath($this->getBasePath() . DIRECTORY_SEPARATOR . 'vendor');
}
return $this->_vendorPath;
}
/**
* Sets the directory that stores vendor files.
* @param string $path the directory that stores vendor files.
*/
public function setVendorPath($path)
{
$this->_vendorPath = Yii::getAlias($path);
}
/** /**
* Returns the time zone used by this application. * Returns the time zone used by this application.
* This is a simple wrapper of PHP function date_default_timezone_get(). * This is a simple wrapper of PHP function date_default_timezone_get().
@ -295,37 +242,6 @@ class Application extends Module
date_default_timezone_set($value); date_default_timezone_set($value);
} }
//
// /**
// * Returns the locale instance.
// * @param string $localeID the locale ID (e.g. en_US). If null, the {@link getLanguage application language ID} will be used.
// * @return CLocale the locale instance
// */
// public function getLocale($localeID = null)
// {
// return CLocale::getInstance($localeID === null ? $this->getLanguage() : $localeID);
// }
//
// /**
// * @return CNumberFormatter the locale-dependent number formatter.
// * The current {@link getLocale application locale} will be used.
// */
// public function getNumberFormatter()
// {
// return $this->getLocale()->getNumberFormatter();
// }
//
// /**
// * Returns the locale-dependent date formatter.
// * @return CDateFormatter the locale-dependent date formatter.
// * The current {@link getLocale application locale} will be used.
// */
// public function getDateFormatter()
// {
// return $this->getLocale()->getDateFormatter();
// }
//
/** /**
* Returns the database connection component. * Returns the database connection component.
* @return \yii\db\Connection the database connection * @return \yii\db\Connection the database connection
@ -390,14 +306,6 @@ class Application extends Module
} }
/** /**
* Sets default path aliases.
*/
public function registerDefaultAliases()
{
Yii::$aliases['@app'] = $this->getBasePath();
}
/**
* Registers the core application components. * Registers the core application components.
* @see setComponents * @see setComponents
*/ */
@ -420,6 +328,45 @@ class Application extends Module
} }
/** /**
* Handles uncaught PHP exceptions.
*
* This method is implemented as a PHP exception handler. It requires
* that constant YII_ENABLE_ERROR_HANDLER be defined true.
*
* @param \Exception $exception exception that is not caught
*/
public function handleException($exception)
{
// disable error capturing to avoid recursive errors while handling exceptions
restore_error_handler();
restore_exception_handler();
try {
$this->logException($exception);
if (($handler = $this->getErrorHandler()) !== null) {
$handler->handle($exception);
} else {
$this->renderException($exception);
}
$this->end(1);
} catch (\Exception $e) {
// exception could be thrown in end() or ErrorHandler::handle()
$msg = (string)$e;
$msg .= "\nPrevious exception:\n";
$msg .= (string)$exception;
if (YII_DEBUG) {
echo $msg;
}
$msg .= "\n\$_SERVER = " . var_export($_SERVER, true);
error_log($msg);
exit(1);
}
}
/**
* Handles PHP execution errors such as warnings, notices. * Handles PHP execution errors such as warnings, notices.
* *
* This method is used as a PHP error handler. It will simply raise an `ErrorException`. * This method is used as a PHP error handler. It will simply raise an `ErrorException`.
@ -450,21 +397,18 @@ class Application extends Module
} }
/** /**
* Handles uncaught PHP exceptions. * Handles fatal PHP errors
*
* This method is implemented as a PHP exception handler. It requires
* that constant YII_ENABLE_ERROR_HANDLER be defined true.
*
* @param \Exception $exception exception that is not caught
*/ */
public function handleException($exception) public function handleFatalError()
{ {
// disable error capturing to avoid recursive errors while handling exceptions if (YII_ENABLE_ERROR_HANDLER) {
restore_error_handler(); $error = error_get_last();
restore_exception_handler();
try { if (ErrorException::isFatalError($error)) {
$this->logException($exception); unset($this->_memoryReserve);
$exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
// use error_log because it's too late to use Yii log
error_log($exception);
if (($handler = $this->getErrorHandler()) !== null) { if (($handler = $this->getErrorHandler()) !== null) {
$handler->handle($exception); $handler->handle($exception);
@ -472,21 +416,10 @@ class Application extends Module
$this->renderException($exception); $this->renderException($exception);
} }
$this->end(1);
} catch (\Exception $e) {
// exception could be thrown in end() or ErrorHandler::handle()
$msg = (string)$e;
$msg .= "\nPrevious exception:\n";
$msg .= (string)$exception;
if (YII_DEBUG) {
echo $msg;
}
$msg .= "\n\$_SERVER = " . var_export($_SERVER, true);
error_log($msg);
exit(1); exit(1);
} }
} }
}
/** /**
* Renders an exception without using rich format. * Renders an exception without using rich format.

144
framework/base/Component.php

@ -7,26 +7,23 @@
namespace yii\base; namespace yii\base;
use Yii;
/** /**
* Component is the base class that provides the *property*, *event* and *behavior* features.
*
* @include @yii/base/Component.md * @include @yii/base/Component.md
*
* @property Behavior[] behaviors list of behaviors currently attached to this component
*
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class Component extends \yii\base\Object class Component extends Object
{ {
/** /**
* @var Vector[] the attached event handlers (event name => handlers) * @var array the attached event handlers (event name => handlers)
*/ */
private $_e; private $_events;
/** /**
* @var Behavior[] the attached behaviors (behavior name => behavior) * @var Behavior[] the attached behaviors (behavior name => behavior)
*/ */
private $_b; private $_behaviors;
/** /**
* Returns the value of a component property. * Returns the value of a component property.
@ -52,7 +49,7 @@ class Component extends \yii\base\Object
} else { } else {
// behavior property // behavior property
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) { if ($behavior->canGetProperty($name)) {
return $behavior->$name; return $behavior->$name;
} }
@ -87,17 +84,16 @@ class Component extends \yii\base\Object
return; return;
} elseif (strncmp($name, 'on ', 3) === 0) { } elseif (strncmp($name, 'on ', 3) === 0) {
// on event: attach event handler // on event: attach event handler
$name = trim(substr($name, 3)); $this->on(trim(substr($name, 3)), $value);
$this->getEventHandlers($name)->add($value);
return; return;
} elseif (strncmp($name, 'as ', 3) === 0) { } elseif (strncmp($name, 'as ', 3) === 0) {
// as behavior: attach behavior // as behavior: attach behavior
$name = trim(substr($name, 3)); $name = trim(substr($name, 3));
$this->attachBehavior($name, $value instanceof Behavior ? $value : \Yii::createObject($value)); $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
} else { } else {
// behavior property // behavior property
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) { if ($behavior->canSetProperty($name)) {
$behavior->$name = $value; $behavior->$name = $value;
return; return;
@ -131,7 +127,7 @@ class Component extends \yii\base\Object
} else { } else {
// behavior property // behavior property
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) { if ($behavior->canGetProperty($name)) {
return $behavior->$name !== null; return $behavior->$name !== null;
} }
@ -161,7 +157,7 @@ class Component extends \yii\base\Object
} else { } else {
// behavior property // behavior property
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) { if ($behavior->canSetProperty($name)) {
$behavior->$name = null; $behavior->$name = null;
return; return;
@ -198,7 +194,7 @@ class Component extends \yii\base\Object
} }
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $object) { foreach ($this->_behaviors as $object) {
if (method_exists($object, $name)) { if (method_exists($object, $name)) {
return call_user_func_array(array($object, $name), $params); return call_user_func_array(array($object, $name), $params);
} }
@ -213,8 +209,8 @@ class Component extends \yii\base\Object
*/ */
public function __clone() public function __clone()
{ {
$this->_e = null; $this->_events = null;
$this->_b = null; $this->_behaviors = null;
} }
/** /**
@ -259,7 +255,7 @@ class Component extends \yii\base\Object
return true; return true;
} else { } else {
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name, $checkVar)) { if ($behavior->canGetProperty($name, $checkVar)) {
return true; return true;
} }
@ -289,7 +285,7 @@ class Component extends \yii\base\Object
return true; return true;
} else { } else {
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name, $checkVar)) { if ($behavior->canSetProperty($name, $checkVar)) {
return true; return true;
} }
@ -337,39 +333,12 @@ class Component extends \yii\base\Object
public function hasEventHandlers($name) public function hasEventHandlers($name)
{ {
$this->ensureBehaviors(); $this->ensureBehaviors();
return isset($this->_e[$name]) && $this->_e[$name]->getCount(); return !empty($this->_events[$name]);
}
/**
* Returns the list of attached event handlers for an event.
* You may manipulate the returned [[Vector]] object by adding or removing handlers.
* For example,
*
* ~~~
* $component->getEventHandlers($eventName)->insertAt(0, $eventHandler);
* ~~~
*
* @param string $name the event name
* @return Vector list of attached event handlers for the event
*/
public function getEventHandlers($name)
{
if (!isset($this->_e[$name])) {
$this->_e[$name] = new Vector;
}
$this->ensureBehaviors();
return $this->_e[$name];
} }
/** /**
* Attaches an event handler to an event. * Attaches an event handler to an event.
* *
* This is equivalent to the following code:
*
* ~~~
* $component->getEventHandlers($eventName)->add($eventHandler);
* ~~~
*
* An event handler must be a valid PHP callback. The followings are * An event handler must be a valid PHP callback. The followings are
* some examples: * some examples:
* *
@ -383,31 +352,53 @@ class Component extends \yii\base\Object
* An event handler must be defined with the following signature, * An event handler must be defined with the following signature,
* *
* ~~~ * ~~~
* function handlerName($event) {} * function ($event)
* ~~~ * ~~~
* *
* where `$event` is an [[Event]] object which includes parameters associated with the event. * where `$event` is an [[Event]] object which includes parameters associated with the event.
* *
* @param string $name the event name * @param string $name the event name
* @param string|array|\Closure $handler the event handler * @param callback $handler the event handler
* @param mixed $data the data to be passed to the event handler when the event is triggered.
* When the event handler is invoked, this data can be accessed via [[Event::data]].
* @see off() * @see off()
*/ */
public function on($name, $handler) public function on($name, $handler, $data = null)
{ {
$this->getEventHandlers($name)->add($handler); $this->ensureBehaviors();
$this->_events[$name][] = array($handler, $data);
} }
/** /**
* Detaches an existing event handler from this component. * Detaches an existing event handler from this component.
* This method is the opposite of [[on()]]. * This method is the opposite of [[on()]].
* @param string $name event name * @param string $name event name
* @param string|array|\Closure $handler the event handler to be removed * @param callback $handler the event handler to be removed.
* If it is null, all handlers attached to the named event will be removed.
* @return boolean if a handler is found and detached * @return boolean if a handler is found and detached
* @see on() * @see on()
*/ */
public function off($name, $handler) public function off($name, $handler = null)
{ {
return $this->getEventHandlers($name)->remove($handler) !== false; $this->ensureBehaviors();
if (isset($this->_events[$name])) {
if ($handler === null) {
$this->_events[$name] = array();
} else {
$removed = false;
foreach ($this->_events[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_events[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_events[$name] = array_values($this->_events[$name]);
}
return $removed;
}
}
return false;
} }
/** /**
@ -420,7 +411,7 @@ class Component extends \yii\base\Object
public function trigger($name, $event = null) public function trigger($name, $event = null)
{ {
$this->ensureBehaviors(); $this->ensureBehaviors();
if (isset($this->_e[$name]) && $this->_e[$name]->getCount()) { if (!empty($this->_events[$name])) {
if ($event === null) { if ($event === null) {
$event = new Event; $event = new Event;
} }
@ -429,8 +420,9 @@ class Component extends \yii\base\Object
} }
$event->handled = false; $event->handled = false;
$event->name = $name; $event->name = $name;
foreach ($this->_e[$name] as $handler) { foreach ($this->_events[$name] as $handler) {
call_user_func($handler, $event); $event->data = $handler[1];
call_user_func($handler[0], $event);
// stop further handling if the event is handled // stop further handling if the event is handled
if ($event instanceof Event && $event->handled) { if ($event instanceof Event && $event->handled) {
return; return;
@ -447,7 +439,7 @@ class Component extends \yii\base\Object
public function getBehavior($name) public function getBehavior($name)
{ {
$this->ensureBehaviors(); $this->ensureBehaviors();
return isset($this->_b[$name]) ? $this->_b[$name] : null; return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null;
} }
/** /**
@ -457,20 +449,20 @@ class Component extends \yii\base\Object
public function getBehaviors() public function getBehaviors()
{ {
$this->ensureBehaviors(); $this->ensureBehaviors();
return $this->_b; return $this->_behaviors;
} }
/** /**
* Attaches a behavior to this component. * Attaches a behavior to this component.
* This method will create the behavior object based on the given * This method will create the behavior object based on the given
* configuration. After that, the behavior object will be attached to * configuration. After that, the behavior object will be attached to
* this component by calling the [[Behavior::attach]] method. * this component by calling the [[Behavior::attach()]] method.
* @param string $name the name of the behavior. * @param string $name the name of the behavior.
* @param string|array|Behavior $behavior the behavior configuration. This can be one of the following: * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following:
* *
* - a [[Behavior]] object * - a [[Behavior]] object
* - a string specifying the behavior class * - a string specifying the behavior class
* - an object configuration array that will be passed to [[\Yii::createObject()]] to create the behavior object. * - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object.
* *
* @return Behavior the behavior object * @return Behavior the behavior object
* @see detachBehavior * @see detachBehavior
@ -498,15 +490,15 @@ class Component extends \yii\base\Object
/** /**
* Detaches a behavior from the component. * Detaches a behavior from the component.
* The behavior's [[Behavior::detach]] method will be invoked. * The behavior's [[Behavior::detach()]] method will be invoked.
* @param string $name the behavior's name. * @param string $name the behavior's name.
* @return Behavior the detached behavior. Null if the behavior does not exist. * @return Behavior the detached behavior. Null if the behavior does not exist.
*/ */
public function detachBehavior($name) public function detachBehavior($name)
{ {
if (isset($this->_b[$name])) { if (isset($this->_behaviors[$name])) {
$behavior = $this->_b[$name]; $behavior = $this->_behaviors[$name];
unset($this->_b[$name]); unset($this->_behaviors[$name]);
$behavior->detach(); $behavior->detach();
return $behavior; return $behavior;
} else { } else {
@ -519,12 +511,12 @@ class Component extends \yii\base\Object
*/ */
public function detachBehaviors() public function detachBehaviors()
{ {
if ($this->_b !== null) { if ($this->_behaviors !== null) {
foreach ($this->_b as $name => $behavior) { foreach ($this->_behaviors as $name => $behavior) {
$this->detachBehavior($name); $this->detachBehavior($name);
} }
} }
$this->_b = array(); $this->_behaviors = array();
} }
/** /**
@ -532,8 +524,8 @@ class Component extends \yii\base\Object
*/ */
public function ensureBehaviors() public function ensureBehaviors()
{ {
if ($this->_b === null) { if ($this->_behaviors === null) {
$this->_b = array(); $this->_behaviors = array();
foreach ($this->behaviors() as $name => $behavior) { foreach ($this->behaviors() as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior); $this->attachBehaviorInternal($name, $behavior);
} }
@ -549,12 +541,12 @@ class Component extends \yii\base\Object
private function attachBehaviorInternal($name, $behavior) private function attachBehaviorInternal($name, $behavior)
{ {
if (!($behavior instanceof Behavior)) { if (!($behavior instanceof Behavior)) {
$behavior = \Yii::createObject($behavior); $behavior = Yii::createObject($behavior);
} }
if (isset($this->_b[$name])) { if (isset($this->_behaviors[$name])) {
$this->_b[$name]->detach(); $this->_behaviors[$name]->detach();
} }
$behavior->attach($this); $behavior->attach($this);
return $this->_b[$name] = $behavior; return $this->_behaviors[$name] = $behavior;
} }
} }

122
framework/base/Controller.php

@ -47,6 +47,11 @@ class Controller extends Component
* by [[run()]] when it is called by [[Application]] to run an action. * by [[run()]] when it is called by [[Application]] to run an action.
*/ */
public $action; public $action;
/**
* @var View the view object that can be used to render views or view files.
*/
private $_view;
/** /**
* @param string $id the ID of this controller * @param string $id the ID of this controller
@ -135,7 +140,7 @@ class Controller extends Component
} elseif ($pos > 0) { } elseif ($pos > 0) {
return $this->module->runAction($route, $params); return $this->module->runAction($route, $params);
} else { } else {
return \Yii::$app->runAction(ltrim($route, '/'), $params); return Yii::$app->runAction(ltrim($route, '/'), $params);
} }
} }
@ -293,6 +298,37 @@ class Controller extends Component
/** /**
* Renders a view and applies layout if available. * Renders a view and applies layout if available.
*
* The view to be rendered can be specified in one of the following formats:
*
* - path alias (e.g. "@app/views/site/index");
* - absolute path within application (e.g. "//site/index"): the view name starts with double slashes.
* The actual view file will be looked for under the [[Application::viewPath|view path]] of the application.
* - absolute path within module (e.g. "/site/index"): the view name starts with a single slash.
* The actual view file will be looked for under the [[Module::viewPath|view path]] of [[module]].
* - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]].
*
* To determine which layout should be applied, the following two steps are conducted:
*
* 1. In the first step, it determines the layout name and the context module:
*
* - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module;
* - If [[layout]] is null, search through all ancestor modules of this controller and find the first
* module whose [[Module::layout|layout]] is not null. The layout and the corresponding module
* are used as the layout name and the context module, respectively. If such a module is not found
* or the corresponding layout is not a string, it will return false, meaning no applicable layout.
*
* 2. In the second step, it determines the actual layout file according to the previously found layout name
* and context module. The layout name can be
*
* - a path alias (e.g. "@app/views/layouts/main");
* - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be
* looked for under the [[Application::layoutPath|layout path]] of the application;
* - a relative path (e.g. "main"): the actual layout layout file will be looked for under the
* [[Module::viewPath|view path]] of the context module.
*
* If the layout name does not contain a file extension, it will use the default one `.php`.
*
* @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name.
* @param array $params the parameters (name-value pairs) that should be made available in the view. * @param array $params the parameters (name-value pairs) that should be made available in the view.
* These parameters will not be available in the layout. * These parameters will not be available in the layout.
@ -301,10 +337,11 @@ class Controller extends Component
*/ */
public function render($view, $params = array()) public function render($view, $params = array())
{ {
$output = Yii::$app->getView()->render($view, $params, $this); $viewFile = $this->findViewFile($view);
$output = $this->getView()->renderFile($viewFile, $params, $this);
$layoutFile = $this->findLayoutFile(); $layoutFile = $this->findLayoutFile();
if ($layoutFile !== false) { if ($layoutFile !== false) {
return Yii::$app->getView()->renderFile($layoutFile, array('content' => $output), $this); return $this->getView()->renderFile($layoutFile, array('content' => $output), $this);
} else { } else {
return $output; return $output;
} }
@ -313,14 +350,15 @@ class Controller extends Component
/** /**
* Renders a view. * Renders a view.
* This method differs from [[render()]] in that it does not apply any layout. * This method differs from [[render()]] in that it does not apply any layout.
* @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. * @param string $view the view name. Please refer to [[render()]] on how to specify a view name.
* @param array $params the parameters (name-value pairs) that should be made available in the view. * @param array $params the parameters (name-value pairs) that should be made available in the view.
* @return string the rendering result. * @return string the rendering result.
* @throws InvalidParamException if the view file does not exist. * @throws InvalidParamException if the view file does not exist.
*/ */
public function renderPartial($view, $params = array()) public function renderPartial($view, $params = array())
{ {
return Yii::$app->getView()->render($view, $params, $this); $viewFile = $this->findViewFile($view);
return $this->getView()->renderFile($viewFile, $params, $this);
} }
/** /**
@ -332,7 +370,30 @@ class Controller extends Component
*/ */
public function renderFile($file, $params = array()) public function renderFile($file, $params = array())
{ {
return Yii::$app->getView()->renderFile($file, $params, $this); return $this->getView()->renderFile($file, $params, $this);
}
/**
* Returns the view object that can be used to render views or view files.
* The [[render()]], [[renderPartial()]] and [[renderFile()]] methods will use
* this view object to implement the actual view rendering.
* @return View the view object that can be used to render views or view files.
*/
public function getView()
{
if ($this->_view === null) {
$this->_view = Yii::$app->getView();
}
return $this->_view;
}
/**
* Sets the view object to be used by this controller.
* @param View $view the view object that can be used to render views or view files.
*/
public function setView($view)
{
$this->_view = $view;
} }
/** /**
@ -347,30 +408,33 @@ class Controller extends Component
} }
/** /**
* Finds the view file based on the given view name.
* @param string $view the view name or the path alias of the view file. Please refer to [[render()]]
* on how to specify this parameter.
* @return string the view file path. Note that the file may not exist.
*/
protected function findViewFile($view)
{
if (strncmp($view, '@', 1) === 0) {
// e.g. "@app/views/main"
$file = Yii::getAlias($view);
} elseif (strncmp($view, '//', 2) === 0) {
// e.g. "//layouts/main"
$file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
} elseif (strncmp($view, '/', 1) === 0) {
// e.g. "/site/index"
$file = $this->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
} else {
$file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view;
}
return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file;
}
/**
* Finds the applicable layout file. * Finds the applicable layout file.
*
* This method locates an applicable layout file via two steps.
*
* In the first step, it determines the layout name and the context module:
*
* - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module;
* - If [[layout]] is null, search through all ancestor modules of this controller and find the first
* module whose [[Module::layout|layout]] is not null. The layout and the corresponding module
* are used as the layout name and the context module, respectively. If such a module is not found
* or the corresponding layout is not a string, it will return false, meaning no applicable layout.
*
* In the second step, it determines the actual layout file according to the previously found layout name
* and context module. The layout name can be
*
* - a path alias (e.g. "@app/views/layouts/main");
* - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be
* looked for under the [[Application::layoutPath|layout path]] of the application;
* - a relative path (e.g. "main"): the actual layout layout file will be looked for under the
* [[Module::viewPath|view path]] of the context module.
*
* If the layout name does not contain a file extension, it will use the default one `.php`.
*
* @return string|boolean the layout file path, or false if layout is not needed. * @return string|boolean the layout file path, or false if layout is not needed.
* Please refer to [[render()]] on how to specify this parameter.
* @throws InvalidParamException if an invalid path alias is used to specify the layout * @throws InvalidParamException if an invalid path alias is used to specify the layout
*/ */
protected function findLayoutFile() protected function findLayoutFile()
@ -399,7 +463,7 @@ class Controller extends Component
$file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view;
} }
if (FileHelper::getExtension($file) === '') { if (pathinfo($file, PATHINFO_EXTENSION) === '') {
$file .= '.php'; $file .= '.php';
} }
return $file; return $file;

58
framework/base/ErrorException.php

@ -7,6 +7,8 @@
namespace yii\base; namespace yii\base;
use Yii;
/** /**
* ErrorException represents a PHP error. * ErrorException represents a PHP error.
* *
@ -33,6 +35,32 @@ class ErrorException extends Exception
$this->severity = $severity; $this->severity = $severity;
$this->file = $filename; $this->file = $filename;
$this->line = $lineno; $this->line = $lineno;
if (function_exists('xdebug_get_function_stack')) {
$trace = array_slice(array_reverse(xdebug_get_function_stack()), 3, -1);
foreach ($trace as &$frame) {
if (!isset($frame['function'])) {
$frame['function'] = 'unknown';
}
// 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
$frame['args'] = array();
if (isset($frame['params']) && !isset($frame['args'])) {
$frame['args'] = $frame['params'];
}
}
$ref = new \ReflectionProperty('Exception', 'trace');
$ref->setAccessible(true);
$ref->setValue($this, $trace);
}
} }
/** /**
@ -51,7 +79,7 @@ class ErrorException extends Exception
* @param array $error error got from error_get_last() * @param array $error error got from error_get_last()
* @return bool if error is one of fatal type * @return bool if error is one of fatal type
*/ */
public static function isFatalErorr($error) public static function isFatalError($error)
{ {
return isset($error['type']) && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING)); return isset($error['type']) && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING));
} }
@ -62,20 +90,20 @@ class ErrorException extends Exception
public function getName() public function getName()
{ {
$names = array( $names = array(
E_ERROR => \Yii::t('yii|Fatal Error'), E_ERROR => Yii::t('yii|Fatal Error'),
E_PARSE => \Yii::t('yii|Parse Error'), E_PARSE => Yii::t('yii|Parse Error'),
E_CORE_ERROR => \Yii::t('yii|Core Error'), E_CORE_ERROR => Yii::t('yii|Core Error'),
E_COMPILE_ERROR => \Yii::t('yii|Compile Error'), E_COMPILE_ERROR => Yii::t('yii|Compile Error'),
E_USER_ERROR => \Yii::t('yii|User Error'), E_USER_ERROR => Yii::t('yii|User Error'),
E_WARNING => \Yii::t('yii|Warning'), E_WARNING => Yii::t('yii|Warning'),
E_CORE_WARNING => \Yii::t('yii|Core Warning'), E_CORE_WARNING => Yii::t('yii|Core Warning'),
E_COMPILE_WARNING => \Yii::t('yii|Compile Warning'), E_COMPILE_WARNING => Yii::t('yii|Compile Warning'),
E_USER_WARNING => \Yii::t('yii|User Warning'), E_USER_WARNING => Yii::t('yii|User Warning'),
E_STRICT => \Yii::t('yii|Strict'), E_STRICT => Yii::t('yii|Strict'),
E_NOTICE => \Yii::t('yii|Notice'), E_NOTICE => Yii::t('yii|Notice'),
E_RECOVERABLE_ERROR => \Yii::t('yii|Recoverable Error'), E_RECOVERABLE_ERROR => Yii::t('yii|Recoverable Error'),
E_DEPRECATED => \Yii::t('yii|Deprecated'), E_DEPRECATED => Yii::t('yii|Deprecated'),
); );
return isset($names[$this->getCode()]) ? $names[$this->getCode()] : \Yii::t('yii|Error'); return isset($names[$this->getCode()]) ? $names[$this->getCode()] : Yii::t('yii|Error');
} }
} }

30
framework/base/ErrorHandler.php

@ -16,8 +16,6 @@ namespace yii\base;
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
use yii\helpers\VarDumper;
class ErrorHandler extends Component class ErrorHandler extends Component
{ {
/** /**
@ -53,6 +51,7 @@ class ErrorHandler extends Component
/** /**
* Handles exception
* @param \Exception $exception * @param \Exception $exception
*/ */
public function handle($exception) public function handle($exception)
@ -63,10 +62,14 @@ class ErrorHandler extends Component
$this->clearOutput(); $this->clearOutput();
} }
$this->render($exception); $this->renderException($exception);
} }
protected function render($exception) /**
* Renders exception
* @param \Exception $exception
*/
protected function renderException($exception)
{ {
if ($this->errorAction !== null) { if ($this->errorAction !== null) {
\Yii::$app->runAction($this->errorAction); \Yii::$app->runAction($this->errorAction);
@ -78,13 +81,19 @@ class ErrorHandler extends Component
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
\Yii::$app->renderException($exception); \Yii::$app->renderException($exception);
} else { } else {
// if there is an error during error rendering it's useful to
// display PHP error in debug mode instead of a blank screen
if(YII_DEBUG) {
ini_set('display_errors', 1);
}
$view = new View; $view = new View;
if (!YII_DEBUG || $exception instanceof UserException) { if (!YII_DEBUG || $exception instanceof UserException) {
$viewName = $this->errorView; $viewName = $this->errorView;
} else { } else {
$viewName = $this->exceptionView; $viewName = $this->exceptionView;
} }
echo $view->render($viewName, array( echo $view->renderFile($viewName, array(
'exception' => $exception, 'exception' => $exception,
), $this); ), $this);
} }
@ -198,6 +207,10 @@ class ErrorHandler extends Component
echo '<div class="code"><pre>' . $output . '</pre></div>'; echo '<div class="code"><pre>' . $output . '</pre></div>';
} }
/**
* Renders calls stack trace
* @param array $trace
*/
public function renderTrace($trace) public function renderTrace($trace)
{ {
$count = 0; $count = 0;
@ -235,6 +248,11 @@ class ErrorHandler extends Component
echo '</table>'; echo '</table>';
} }
/**
* Converts special characters to HTML entities
* @param string $text text to encode
* @return string
*/
public function htmlEncode($text) public function htmlEncode($text)
{ {
return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset); return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset);
@ -255,7 +273,7 @@ class ErrorHandler extends Component
{ {
$view = new View; $view = new View;
$name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView;
echo $view->render($name, array( echo $view->renderFile($name, array(
'exception' => $exception, 'exception' => $exception,
), $this); ), $this);
} }

9
framework/base/Event.php

@ -15,12 +15,14 @@ namespace yii\base;
* And the [[handled]] property indicates if the event is handled. * And the [[handled]] property indicates if the event is handled.
* If an event handler sets [[handled]] to be true, the rest of the * If an event handler sets [[handled]] to be true, the rest of the
* uninvoked handlers will no longer be called to handle the event. * uninvoked handlers will no longer be called to handle the event.
* Additionally, an event may specify extra parameters via the [[data]] property. *
* Additionally, when attaching an event handler, extra data may be passed
* and be available via the [[data]] property when the event handler is invoked.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class Event extends \yii\base\Object class Event extends Object
{ {
/** /**
* @var string the event name. This property is set by [[Component::trigger()]]. * @var string the event name. This property is set by [[Component::trigger()]].
@ -39,7 +41,8 @@ class Event extends \yii\base\Object
*/ */
public $handled = false; public $handled = false;
/** /**
* @var mixed extra custom data associated with the event. * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler.
* Note that this varies according to which event handler is currently executing.
*/ */
public $data; public $data;
} }

5
framework/base/HttpException.php

@ -29,11 +29,12 @@ class HttpException extends UserException
* @param integer $status HTTP status code, such as 404, 500, etc. * @param integer $status HTTP status code, such as 404, 500, etc.
* @param string $message error message * @param string $message error message
* @param integer $code error code * @param integer $code error code
* @param \Exception $previous The previous exception used for the exception chaining.
*/ */
public function __construct($status, $message = null, $code = 0) public function __construct($status, $message = null, $code = 0, \Exception $previous = null)
{ {
$this->statusCode = $status; $this->statusCode = $status;
parent::__construct($message, $code); parent::__construct($message, $code, $previous);
} }
/** /**

24
framework/base/Model.php

@ -8,8 +8,8 @@
namespace yii\base; namespace yii\base;
use yii\helpers\StringHelper; use yii\helpers\StringHelper;
use yii\validators\Validator;
use yii\validators\RequiredValidator; use yii\validators\RequiredValidator;
use yii\validators\Validator;
/** /**
* Model is the base class for data models. * Model is the base class for data models.
@ -169,6 +169,26 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
} }
/** /**
* Returns the form name that this model class should use.
*
* The form name is mainly used by [[\yii\web\ActiveForm]] to determine how to name
* the input fields for the attributes in a model. If the form name is "A" and an attribute
* name is "b", then the corresponding input name would be "A[b]". If the form name is
* an empty string, then the input name would be "b".
*
* By default, this method returns the model class name (without the namespace part)
* as the form name. You may override it when the model is used in different forms.
*
* @return string the form name of this model class.
*/
public function formName()
{
$class = get_class($this);
$pos = strrpos($class, '\\');
return $pos === false ? $class : substr($class, $pos + 1);
}
/**
* Returns the list of attribute names. * Returns the list of attribute names.
* By default, this method returns all public non-static properties of the class. * By default, this method returns all public non-static properties of the class.
* You may override this method to change the default behavior. * You may override this method to change the default behavior.
@ -541,7 +561,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
public function onUnsafeAttribute($name, $value) public function onUnsafeAttribute($name, $value)
{ {
if (YII_DEBUG) { if (YII_DEBUG) {
\Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __CLASS__); \Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__);
} }
} }

53
framework/base/Module.php

@ -170,7 +170,6 @@ abstract class Module extends Component
*/ */
public function init() public function init()
{ {
Yii::setAlias('@' . $this->id, $this->getBasePath());
$this->preloadComponents(); $this->preloadComponents();
} }
@ -208,11 +207,17 @@ abstract class Module extends Component
* Sets the root directory of the module. * Sets the root directory of the module.
* This method can only be invoked at the beginning of the constructor. * This method can only be invoked at the beginning of the constructor.
* @param string $path the root directory of the module. This can be either a directory name or a path alias. * @param string $path the root directory of the module. This can be either a directory name or a path alias.
* @throws Exception if the directory does not exist. * @throws InvalidParamException if the directory does not exist.
*/ */
public function setBasePath($path) public function setBasePath($path)
{ {
$this->_basePath = FileHelper::ensureDirectory($path); $path = Yii::getAlias($path);
$p = realpath($path);
if ($p !== false && is_dir($p)) {
$this->_basePath = $p;
} else {
throw new InvalidParamException("The directory does not exist: $path");
}
} }
/** /**
@ -237,7 +242,7 @@ abstract class Module extends Component
*/ */
public function setControllerPath($path) public function setControllerPath($path)
{ {
$this->_controllerPath = FileHelper::ensureDirectory($path); $this->_controllerPath = Yii::getAlias($path);
} }
/** /**
@ -260,7 +265,7 @@ abstract class Module extends Component
*/ */
public function setViewPath($path) public function setViewPath($path)
{ {
$this->_viewPath = FileHelper::ensureDirectory($path); $this->_viewPath = Yii::getAlias($path);
} }
/** /**
@ -283,20 +288,7 @@ abstract class Module extends Component
*/ */
public function setLayoutPath($path) public function setLayoutPath($path)
{ {
$this->_layoutPath = FileHelper::ensureDirectory($path); $this->_layoutPath = Yii::getAlias($path);
}
/**
* Imports the specified path aliases.
* This method is provided so that you can import a set of path aliases when configuring a module.
* The path aliases will be imported by calling [[Yii::import()]].
* @param array $aliases list of path aliases to be imported
*/
public function setImport($aliases)
{
foreach ($aliases as $alias) {
Yii::import($alias);
}
} }
/** /**
@ -346,7 +338,7 @@ abstract class Module extends Component
if ($this->_modules[$id] instanceof Module) { if ($this->_modules[$id] instanceof Module) {
return $this->_modules[$id]; return $this->_modules[$id];
} elseif ($load) { } elseif ($load) {
Yii::trace("Loading module: $id", __CLASS__); Yii::trace("Loading module: $id", __METHOD__);
return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this);
} }
} }
@ -452,7 +444,7 @@ abstract class Module extends Component
if ($this->_components[$id] instanceof Component) { if ($this->_components[$id] instanceof Component) {
return $this->_components[$id]; return $this->_components[$id];
} elseif ($load) { } elseif ($load) {
Yii::trace("Loading component: $id", __CLASS__); Yii::trace("Loading component: $id", __METHOD__);
return $this->_components[$id] = Yii::createObject($this->_components[$id]); return $this->_components[$id] = Yii::createObject($this->_components[$id]);
} }
} }
@ -580,8 +572,9 @@ abstract class Module extends Component
* instance of it. * instance of it.
* *
* @param string $route the route consisting of module, controller and action IDs. * @param string $route the route consisting of module, controller and action IDs.
* @return array|boolean if the controller is created successfully, it will be returned together * @return array|boolean If the controller is created successfully, it will be returned together
* with the remainder of the route which represents the action ID. Otherwise false will be returned. * with the requested action ID. Otherwise false will be returned.
* @throws InvalidConfigException if the controller class and its file do not match.
*/ */
public function createController($route) public function createController($route)
{ {
@ -605,16 +598,16 @@ abstract class Module extends Component
$controller = Yii::createObject($this->controllerMap[$id], $id, $this); $controller = Yii::createObject($this->controllerMap[$id], $id, $this);
} elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) {
$className = StringHelper::id2camel($id) . 'Controller'; $className = StringHelper::id2camel($id) . 'Controller';
$classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php';
if (is_file($classFile)) { if (!is_file($classFile)) {
$className = $this->controllerNamespace . '\\' . $className; return false;
if (!class_exists($className, false)) {
require($classFile);
} }
if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { $className = ltrim($this->controllerNamespace . '\\' . $className, '\\');
Yii::$classMap[$className] = $classFile;
if (is_subclass_of($className, 'yii\base\Controller')) {
$controller = new $className($id, $this); $controller = new $className($id, $this);
} } elseif (YII_DEBUG) {
throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller.");
} }
} }

3
framework/base/Object.php

@ -8,10 +8,7 @@
namespace yii\base; namespace yii\base;
/** /**
* Object is the base class that provides the *property* feature.
*
* @include @yii/base/Object.md * @include @yii/base/Object.md
*
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */

22
framework/base/Response.php

@ -13,28 +13,38 @@ namespace yii\base;
*/ */
class Response extends Component class Response extends Component
{ {
/**
* Starts output buffering
*/
public function beginOutput() public function beginOutput()
{ {
ob_start(); ob_start();
ob_implicit_flush(false); ob_implicit_flush(false);
} }
/**
* Returns contents of the output buffer and discards it
* @return string output buffer contents
*/
public function endOutput() public function endOutput()
{ {
return ob_get_clean(); return ob_get_clean();
} }
/**
* Returns contents of the output buffer
* @return string output buffer contents
*/
public function getOutput() public function getOutput()
{ {
return ob_get_contents(); return ob_get_contents();
} }
public function cleanOutput() /**
{ * Discards the output buffer
ob_clean(); * @param boolean $all if true recursively discards all output buffers used
} */
public function cleanOutput($all = true)
public function removeOutput($all = true)
{ {
if ($all) { if ($all) {
for ($level = ob_get_level(); $level > 0; --$level) { for ($level = ob_get_level(); $level > 0; --$level) {

37
framework/base/Theme.php

@ -33,11 +33,17 @@ use yii\helpers\FileHelper;
class Theme extends Component class Theme extends Component
{ {
/** /**
* @var string the root path of this theme. * @var string the root path or path alias of this theme. All resources of this theme are located
* under this directory. This property must be set if [[pathMap]] is not set.
* @see pathMap * @see pathMap
*/ */
public $basePath; public $basePath;
/** /**
* @var string the base URL (or path alias) for this theme. All resources of this theme are considered
* to be under this base URL. This property must be set. It is mainly used by [[getUrl()]].
*/
public $baseUrl;
/**
* @var array the mapping between view directories and their corresponding themed versions. * @var array the mapping between view directories and their corresponding themed versions.
* If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]]. * If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]].
* This property is used by [[applyTo()]] when a view is trying to apply the theme. * This property is used by [[applyTo()]] when a view is trying to apply the theme.
@ -45,7 +51,6 @@ class Theme extends Component
*/ */
public $pathMap; public $pathMap;
private $_baseUrl;
/** /**
* Initializes the theme. * Initializes the theme.
@ -56,10 +61,10 @@ class Theme extends Component
parent::init(); parent::init();
if (empty($this->pathMap)) { if (empty($this->pathMap)) {
if ($this->basePath !== null) { if ($this->basePath !== null) {
$this->basePath = FileHelper::ensureDirectory($this->basePath); $this->basePath = Yii::getAlias($this->basePath);
$this->pathMap = array(Yii::$app->getBasePath() => $this->basePath); $this->pathMap = array(Yii::$app->getBasePath() => $this->basePath);
} else { } else {
throw new InvalidConfigException("Theme::basePath must be set."); throw new InvalidConfigException('The "basePath" property must be set.');
} }
} }
$paths = array(); $paths = array();
@ -69,25 +74,11 @@ class Theme extends Component
$paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR; $paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR;
} }
$this->pathMap = $paths; $this->pathMap = $paths;
if ($this->baseUrl === null) {
throw new InvalidConfigException('The "baseUrl" property must be set.');
} else {
$this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/');
} }
/**
* Returns the base URL for this theme.
* The method [[getUrl()]] will prefix this to the given URL.
* @return string the base URL for this theme.
*/
public function getBaseUrl()
{
return $this->_baseUrl;
}
/**
* Sets the base URL for this theme.
* @param string $value the base URL for this theme.
*/
public function setBaseUrl($value)
{
$this->_baseUrl = rtrim(Yii::getAlias($value), '/');
} }
/** /**
@ -112,7 +103,7 @@ class Theme extends Component
} }
/** /**
* Converts a relative URL into an absolute URL using [[basePath]]. * Converts a relative URL into an absolute URL using [[baseUrl]].
* @param string $url the relative URL to be converted. * @param string $url the relative URL to be converted.
* @return string the absolute URL * @return string the absolute URL
*/ */

26
framework/base/UnknownClassException.php

@ -0,0 +1,26 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\base;
/**
* UnknownClassException represents an exception caused by accessing an unknown class.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class UnknownClassException extends Exception
{
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return \Yii::t('yii|Unknown Class');
}
}

2
framework/base/UnknownMethodException.php

@ -8,7 +8,7 @@
namespace yii\base; namespace yii\base;
/** /**
* UnknownMethodException represents an exception caused by accessing unknown object methods. * UnknownMethodException represents an exception caused by accessing an unknown object method.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0

519
framework/base/View.php

@ -10,6 +10,7 @@ namespace yii\base;
use Yii; use Yii;
use yii\base\Application; use yii\base\Application;
use yii\helpers\FileHelper; use yii\helpers\FileHelper;
use yii\helpers\Html;
/** /**
* View represents a view object in the MVC pattern. * View represents a view object in the MVC pattern.
@ -22,7 +23,46 @@ use yii\helpers\FileHelper;
class View extends Component class View extends Component
{ {
/** /**
* @var object the object that owns this view. This can be a controller, a widget, or any other object. * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file.
*/
const EVENT_BEFORE_RENDER = 'beforeRender';
/**
* @event ViewEvent an event that is triggered by [[renderFile()]] right after it renders a view file.
*/
const EVENT_AFTER_RENDER = 'afterRender';
/**
* The location of registered JavaScript code block or files.
* This means the location is in the head section.
*/
const POS_HEAD = 1;
/**
* The location of registered JavaScript code block or files.
* This means the location is at the beginning of the body section.
*/
const POS_BEGIN = 2;
/**
* The location of registered JavaScript code block or files.
* This means the location is at the end of the body section.
*/
const POS_END = 3;
/**
* This is internally used as the placeholder for receiving the content registered for the head section.
*/
const PL_HEAD = '<![CDATA[YII-BLOCK-HEAD]]>';
/**
* This is internally used as the placeholder for receiving the content registered for the beginning of the body section.
*/
const PL_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN]]>';
/**
* This is internally used as the placeholder for receiving the content registered for the end of the body section.
*/
const PL_BODY_END = '<![CDATA[YII-BLOCK-BODY-END]]>';
/**
* @var object the context under which the [[renderFile()]] method is being invoked.
* This can be a controller, a widget, or any other object.
*/ */
public $context; public $context;
/** /**
@ -35,31 +75,75 @@ class View extends Component
*/ */
public $renderer; public $renderer;
/** /**
* @var Theme|array the theme object or the configuration array for creating the theme. * @var Theme|array the theme object or the configuration array for creating the theme object.
* If not set, it means theming is not enabled. * If not set, it means theming is not enabled.
*/ */
public $theme; public $theme;
/** /**
* @var array a list of named output clips. You can call [[beginClip()]] and [[endClip()]] * @var array a list of named output blocks. The keys are the block names and the values
* to capture small fragments of a view. They can be later accessed at somewhere else * are the corresponding block content. You can call [[beginBlock()]] and [[endBlock()]]
* to capture small fragments of a view. They can be later accessed somewhere else
* through this property. * through this property.
*/ */
public $clips; public $blocks;
/** /**
* @var Widget[] the widgets that are currently being rendered (not ended). This property * @var Widget[] the widgets that are currently being rendered (not ended). This property
* is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly.
* @internal
*/ */
public $widgetStack = array(); public $widgetStack = array();
/** /**
* @var array a list of currently active fragment cache widgets. This property * @var array a list of currently active fragment cache widgets. This property
* is used internally to implement the content caching feature. Do not modify it. * is used internally to implement the content caching feature. Do not modify it directly.
* @internal
*/ */
public $cacheStack = array(); public $cacheStack = array();
/** /**
* @var array a list of placeholders for embedding dynamic contents. This property * @var array a list of placeholders for embedding dynamic contents. This property
* is used internally to implement the content caching feature. Do not modify it. * is used internally to implement the content caching feature. Do not modify it directly.
* @internal
*/ */
public $dynamicPlaceholders = array(); public $dynamicPlaceholders = array();
/**
* @var array the registered asset bundles. The keys are the bundle names, and the values
* are the corresponding [[AssetBundle]] objects.
* @see registerAssetBundle
*/
public $assetBundles;
/**
* @var string the page title
*/
public $title;
/**
* @var array the registered meta tags.
* @see registerMetaTag
*/
public $metaTags;
/**
* @var array the registered link tags.
* @see registerLinkTag
*/
public $linkTags;
/**
* @var array the registered CSS code blocks.
* @see registerCss
*/
public $css;
/**
* @var array the registered CSS files.
* @see registerCssFile
*/
public $cssFiles;
/**
* @var array the registered JS code blocks
* @see registerJs
*/
public $js;
/**
* @var array the registered JS files.
* @see registerJsFile
*/
public $jsFiles;
/** /**
@ -79,22 +163,29 @@ class View extends Component
/** /**
* Renders a view. * Renders a view.
* *
* This method will call [[findViewFile()]] to convert the view name into the corresponding view * This method delegates the call to the [[context]] object:
* file path, and it will then call [[renderFile()]] to render the view.
* *
* @param string $view the view name. Please refer to [[findViewFile()]] on how to specify this parameter. * - If [[context]] is a controller, the [[Controller::renderPartial()]] method will be called;
* - If [[context]] is a widget, the [[Widget::render()]] method will be called;
* - Otherwise, an InvalidCallException exception will be thrown.
*
* @param string $view the view name. Please refer to [[Controller::findViewFile()]]
* and [[Widget::findViewFile()]] on how to specify this parameter.
* @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file.
* @param object $context the context that the view should use for rendering the view. If null,
* existing [[context]] will be used.
* @return string the rendering result * @return string the rendering result
* @throws InvalidCallException if [[context]] is neither a controller nor a widget.
* @throws InvalidParamException if the view cannot be resolved or the view file does not exist. * @throws InvalidParamException if the view cannot be resolved or the view file does not exist.
* @see renderFile * @see renderFile
* @see findViewFile
*/ */
public function render($view, $params = array(), $context = null) public function render($view, $params = array())
{ {
$viewFile = $this->findViewFile($context, $view); if ($this->context instanceof Controller) {
return $this->renderFile($viewFile, $params, $context); return $this->context->renderPartial($view, $params);
} elseif ($this->context instanceof Widget) {
return $this->context->render($view, $params);
} else {
throw new InvalidCallException('View::render() is not supported for the current context.');
}
} }
/** /**
@ -133,11 +224,15 @@ class View extends Component
$this->context = $context; $this->context = $context;
} }
$output = '';
if ($this->beforeRender($viewFile)) {
if ($this->renderer !== null) { if ($this->renderer !== null) {
$output = $this->renderer->render($this, $viewFile, $params); $output = $this->renderer->render($this, $viewFile, $params);
} else { } else {
$output = $this->renderPhpFile($viewFile, $params); $output = $this->renderPhpFile($viewFile, $params);
} }
$this->afterRender($viewFile, $output);
}
$this->context = $oldContext; $this->context = $oldContext;
@ -145,6 +240,38 @@ class View extends Component
} }
/** /**
* This method is invoked right before [[renderFile()]] renders a view file.
* The default implementation will trigger the [[EVENT_BEFORE_RENDER]] event.
* If you override this method, make sure you call the parent implementation first.
* @param string $viewFile the view file to be rendered
* @return boolean whether to continue rendering the view file.
*/
public function beforeRender($viewFile)
{
$event = new ViewEvent($viewFile);
$this->trigger(self::EVENT_BEFORE_RENDER, $event);
return $event->isValid;
}
/**
* This method is invoked right after [[renderFile()]] renders a view file.
* The default implementation will trigger the [[EVENT_AFTER_RENDER]] event.
* If you override this method, make sure you call the parent implementation first.
* @param string $viewFile the view file to be rendered
* @param string $output the rendering result of the view file. Updates to this parameter
* will be passed back and returned by [[renderFile()]].
*/
public function afterRender($viewFile, &$output)
{
if ($this->hasEventHandlers(self::EVENT_AFTER_RENDER)) {
$event = new ViewEvent($viewFile);
$event->output = $output;
$this->trigger(self::EVENT_AFTER_RENDER, $event);
$output = $event->output;
}
}
/**
* Renders a view file as a PHP script. * Renders a view file as a PHP script.
* *
* This method treats the view file as a PHP script and includes the file. * This method treats the view file as a PHP script and includes the file.
@ -179,7 +306,7 @@ class View extends Component
{ {
if (!empty($this->cacheStack)) { if (!empty($this->cacheStack)) {
$n = count($this->dynamicPlaceholders); $n = count($this->dynamicPlaceholders);
$placeholder = "<![CDATA[YDP-$n]]>"; $placeholder = "<![CDATA[YII-DYNAMIC-$n]]>";
$this->addDynamicPlaceholder($placeholder, $statements); $this->addDynamicPlaceholder($placeholder, $statements);
return $placeholder; return $placeholder;
} else { } else {
@ -213,49 +340,6 @@ class View extends Component
} }
/** /**
* Finds the view file based on the given view name.
*
* A view name can be specified in one of the following formats:
*
* - path alias (e.g. "@app/views/site/index");
* - absolute path within application (e.g. "//site/index"): the view name starts with double slashes.
* The actual view file will be looked for under the [[Application::viewPath|view path]] of the application.
* - absolute path within module (e.g. "/site/index"): the view name starts with a single slash.
* The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently
* active module.
* - relative path (e.g. "index"): the actual view file will be looked for under [[Controller::viewPath|viewPath]]
* of the context object, assuming the context is either a [[Controller]] or a [[Widget]].
*
* If the view name does not contain a file extension, it will use the default one `.php`.
*
* @param object $context the view context object
* @param string $view the view name or the path alias of the view file.
* @return string the view file path. Note that the file may not exist.
* @throws InvalidParamException if the view file is an invalid path alias or the context cannot be
* used to determine the actual view file corresponding to the specified view.
*/
protected function findViewFile($context, $view)
{
if (strncmp($view, '@', 1) === 0) {
// e.g. "@app/views/main"
$file = Yii::getAlias($view);
} elseif (strncmp($view, '//', 2) === 0) {
// e.g. "//layouts/main"
$file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
} elseif (strncmp($view, '/', 1) === 0) {
// e.g. "/site/index"
$file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
} elseif ($context instanceof Controller || $context instanceof Widget) {
/** @var $context Controller|Widget */
$file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view;
} else {
throw new InvalidParamException("Unable to resolve the view file for '$view'.");
}
return FileHelper::getExtension($file) === '' ? $file . '.php' : $file;
}
/**
* Creates a widget. * Creates a widget.
* This method will use [[Yii::createObject()]] to create the widget. * This method will use [[Yii::createObject()]] to create the widget.
* @param string $class the widget class name or path alias * @param string $class the widget class name or path alias
@ -265,7 +349,10 @@ class View extends Component
public function createWidget($class, $properties = array()) public function createWidget($class, $properties = array())
{ {
$properties['class'] = $class; $properties['class'] = $class;
return Yii::createObject($properties, $this->context); if (!isset($properties['view'])) {
$properties['view'] = $this;
}
return Yii::createObject($properties, $this);
} }
/** /**
@ -328,44 +415,50 @@ class View extends Component
} }
/** /**
* Begins recording a clip. * Begins recording a block.
* This method is a shortcut to beginning [[yii\widgets\Clip]] * This method is a shortcut to beginning [[yii\widgets\Block]]
* @param string $id the clip ID. * @param string $id the block ID.
* @param boolean $renderInPlace whether to render the clip content in place. * @param boolean $renderInPlace whether to render the block content in place.
* Defaults to false, meaning the captured clip will not be displayed. * Defaults to false, meaning the captured block will not be displayed.
* @return \yii\widgets\Clip the Clip widget instance * @return \yii\widgets\Block the Block widget instance
* @see \yii\widgets\Clip
*/ */
public function beginClip($id, $renderInPlace = false) public function beginBlock($id, $renderInPlace = false)
{ {
return $this->beginWidget('yii\widgets\Clip', array( return $this->beginWidget('yii\widgets\Block', array(
'id' => $id, 'id' => $id,
'renderInPlace' => $renderInPlace, 'renderInPlace' => $renderInPlace,
'view' => $this,
)); ));
} }
/** /**
* Ends recording a clip. * Ends recording a block.
*/ */
public function endClip() public function endBlock()
{ {
$this->endWidget(); $this->endWidget();
} }
/** /**
* Begins the rendering of content that is to be decorated by the specified view. * Begins the rendering of content that is to be decorated by the specified view.
* @param string $view the name of the view that will be used to decorate the content enclosed by this widget. * This method can be used to implement nested layout. For example, a layout can be embedded
* Please refer to [[View::findViewFile()]] on how to set this property. * in another layout file specified as '@app/view/layouts/base' like the following:
*
* ~~~
* <?php $this->beginContent('@app/view/layouts/base'); ?>
* ...layout content here...
* <?php $this->endContent(); ?>
* ~~~
*
* @param string $viewFile the view file that will be used to decorate the content enclosed by this widget.
* This can be specified as either the view file path or path alias.
* @param array $params the variables (name=>value) to be extracted and made available in the decorative view. * @param array $params the variables (name=>value) to be extracted and made available in the decorative view.
* @return \yii\widgets\ContentDecorator the ContentDecorator widget instance * @return \yii\widgets\ContentDecorator the ContentDecorator widget instance
* @see \yii\widgets\ContentDecorator * @see \yii\widgets\ContentDecorator
*/ */
public function beginContent($view, $params = array()) public function beginContent($viewFile, $params = array())
{ {
return $this->beginWidget('yii\widgets\ContentDecorator', array( return $this->beginWidget('yii\widgets\ContentDecorator', array(
'view' => $this, 'viewFile' => $viewFile,
'viewName' => $view,
'params' => $params, 'params' => $params,
)); ));
} }
@ -400,7 +493,6 @@ class View extends Component
public function beginCache($id, $properties = array()) public function beginCache($id, $properties = array())
{ {
$properties['id'] = $id; $properties['id'] = $id;
$properties['view'] = $this;
/** @var $cache \yii\widgets\FragmentCache */ /** @var $cache \yii\widgets\FragmentCache */
$cache = $this->beginWidget('yii\widgets\FragmentCache', $properties); $cache = $this->beginWidget('yii\widgets\FragmentCache', $properties);
if ($cache->getCachedContent() !== false) { if ($cache->getCachedContent() !== false) {
@ -418,4 +510,273 @@ class View extends Component
{ {
$this->endWidget(); $this->endWidget();
} }
private $_assetManager;
/**
* Registers the asset manager being used by this view object.
* @return \yii\web\AssetManager the asset manager. Defaults to the "assetManager" application component.
*/
public function getAssetManager()
{
return $this->_assetManager ?: Yii::$app->getAssetManager();
}
/**
* Sets the asset manager.
* @param \yii\web\AssetManager $value the asset manager
*/
public function setAssetManager($value)
{
$this->_assetManager = $value;
}
/**
* Marks the beginning of an HTML page.
*/
public function beginPage()
{
ob_start();
ob_implicit_flush(false);
}
/**
* Marks the ending of an HTML page.
*/
public function endPage()
{
$content = ob_get_clean();
echo strtr($content, array(
self::PL_HEAD => $this->renderHeadHtml(),
self::PL_BODY_BEGIN => $this->renderBodyBeginHtml(),
self::PL_BODY_END => $this->renderBodyEndHtml(),
));
unset(
$this->assetBundles,
$this->metaTags,
$this->linkTags,
$this->css,
$this->cssFiles,
$this->js,
$this->jsFiles
);
}
/**
* Marks the beginning of an HTML body section.
*/
public function beginBody()
{
echo self::PL_BODY_BEGIN;
}
/**
* Marks the ending of an HTML body section.
*/
public function endBody()
{
echo self::PL_BODY_END;
}
/**
* Marks the position of an HTML head section.
*/
public function head()
{
echo self::PL_HEAD;
}
/**
* Registers the named asset bundle.
* All dependent asset bundles will be registered.
* @param string $name the name of the asset bundle.
* @throws InvalidConfigException if the asset bundle does not exist or a circular dependency is detected
*/
public function registerAssetBundle($name)
{
if (!isset($this->assetBundles[$name])) {
$am = $this->getAssetManager();
$bundle = $am->getBundle($name);
if ($bundle !== null) {
$this->assetBundles[$name] = false;
$bundle->registerAssets($this);
$this->assetBundles[$name] = true;
} else {
throw new InvalidConfigException("Unknown asset bundle: $name");
}
} elseif ($this->assetBundles[$name] === false) {
throw new InvalidConfigException("A circular dependency is detected for bundle '$name'.");
}
}
/**
* Registers a meta tag.
* @param array $options the HTML attributes for the meta tag.
* @param string $key the key that identifies the meta tag. If two meta tags are registered
* with the same key, the latter will overwrite the former. If this is null, the new meta tag
* will be appended to the existing ones.
*/
public function registerMetaTag($options, $key = null)
{
if ($key === null) {
$this->metaTags[] = Html::tag('meta', '', $options);
} else {
$this->metaTags[$key] = Html::tag('meta', '', $options);
}
}
/**
* Registers a link tag.
* @param array $options the HTML attributes for the link tag.
* @param string $key the key that identifies the link tag. If two link tags are registered
* with the same key, the latter will overwrite the former. If this is null, the new link tag
* will be appended to the existing ones.
*/
public function registerLinkTag($options, $key = null)
{
if ($key === null) {
$this->linkTags[] = Html::tag('link', '', $options);
} else {
$this->linkTags[$key] = Html::tag('link', '', $options);
}
}
/**
* Registers a CSS code block.
* @param string $css the CSS code block to be registered
* @param array $options the HTML attributes for the style tag.
* @param string $key the key that identifies the CSS code block. If null, it will use
* $css as the key. If two CSS code blocks are registered with the same key, the latter
* will overwrite the former.
*/
public function registerCss($css, $options = array(), $key = null)
{
$key = $key ?: $css;
$this->css[$key] = Html::style($css, $options);
}
/**
* Registers a CSS file.
* @param string $url the CSS file to be registered.
* @param array $options the HTML attributes for the link tag.
* @param string $key the key that identifies the CSS script file. If null, it will use
* $url as the key. If two CSS files are registered with the same key, the latter
* will overwrite the former.
*/
public function registerCssFile($url, $options = array(), $key = null)
{
$key = $key ?: $url;
$this->cssFiles[$key] = Html::cssFile($url, $options);
}
/**
* Registers a JS code block.
* @param string $js the JS code block to be registered
* @param array $options the HTML attributes for the script tag. A special option
* named "position" is supported which specifies where the JS script tag should be inserted
* in a page. The possible values of "position" are:
*
* - [[POS_HEAD]]: in the head section
* - [[POS_BEGIN]]: at the beginning of the body section
* - [[POS_END]]: at the end of the body section
*
* @param string $key the key that identifies the JS code block. If null, it will use
* $js as the key. If two JS code blocks are registered with the same key, the latter
* will overwrite the former.
*/
public function registerJs($js, $options = array(), $key = null)
{
$position = isset($options['position']) ? $options['position'] : self::POS_END;
unset($options['position']);
$key = $key ?: $js;
$this->js[$position][$key] = Html::script($js, $options);
}
/**
* Registers a JS file.
* @param string $url the JS file to be registered.
* @param array $options the HTML attributes for the script tag. A special option
* named "position" is supported which specifies where the JS script tag should be inserted
* in a page. The possible values of "position" are:
*
* - [[POS_HEAD]]: in the head section
* - [[POS_BEGIN]]: at the beginning of the body section
* - [[POS_END]]: at the end of the body section
*
* @param string $key the key that identifies the JS script file. If null, it will use
* $url as the key. If two JS files are registered with the same key, the latter
* will overwrite the former.
*/
public function registerJsFile($url, $options = array(), $key = null)
{
$position = isset($options['position']) ? $options['position'] : self::POS_END;
unset($options['position']);
$key = $key ?: $url;
$this->jsFiles[$position][$key] = Html::jsFile($url, $options);
}
/**
* Renders the content to be inserted in the head section.
* The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files.
* @return string the rendered content
*/
protected function renderHeadHtml()
{
$lines = array();
if (!empty($this->metaTags)) {
$lines[] = implode("\n", $this->cssFiles);
}
if (!empty($this->linkTags)) {
$lines[] = implode("\n", $this->cssFiles);
}
if (!empty($this->cssFiles)) {
$lines[] = implode("\n", $this->cssFiles);
}
if (!empty($this->css)) {
$lines[] = implode("\n", $this->css);
}
if (!empty($this->jsFiles[self::POS_HEAD])) {
$lines[] = implode("\n", $this->jsFiles[self::POS_HEAD]);
}
if (!empty($this->js[self::POS_HEAD])) {
$lines[] = implode("\n", $this->js[self::POS_HEAD]);
}
return implode("\n", $lines);
}
/**
* Renders the content to be inserted at the beginning of the body section.
* The content is rendered using the registered JS code blocks and files.
* @return string the rendered content
*/
protected function renderBodyBeginHtml()
{
$lines = array();
if (!empty($this->jsFiles[self::POS_BEGIN])) {
$lines[] = implode("\n", $this->jsFiles[self::POS_BEGIN]);
}
if (!empty($this->js[self::POS_BEGIN])) {
$lines[] = implode("\n", $this->js[self::POS_BEGIN]);
}
return implode("\n", $lines);
}
/**
* Renders the content to be inserted at the end of the body section.
* The content is rendered using the registered JS code blocks and files.
* @return string the rendered content
*/
protected function renderBodyEndHtml()
{
$lines = array();
if (!empty($this->jsFiles[self::POS_END])) {
$lines[] = implode("\n", $this->jsFiles[self::POS_END]);
}
if (!empty($this->js[self::POS_END])) {
$lines[] = implode("\n", $this->js[self::POS_END]);
}
return implode("\n", $lines);
}
} }

44
framework/base/ViewEvent.php

@ -0,0 +1,44 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\base;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class ViewEvent extends Event
{
/**
* @var string the rendering result of [[View::renderFile()]].
* Event handlers may modify this property and the modified output will be
* returned by [[View::renderFile()]]. This property is only used
* by [[View::EVENT_AFTER_RENDER]] event.
*/
public $output;
/**
* @var string the view file path that is being rendered by [[View::renderFile()]].
*/
public $viewFile;
/**
* @var boolean whether to continue rendering the view file. Event handlers of
* [[View::EVENT_BEFORE_RENDER]] may set this property to decide whether
* to continue rendering the current view file.
*/
public $isValid = true;
/**
* Constructor.
* @param string $viewFile the view file path that is being rendered by [[View::renderFile()]].
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($viewFile, $config = array())
{
$this->viewFile = $viewFile;
parent::__construct($config);
}
}

57
framework/base/Widget.php

@ -19,9 +19,11 @@ use yii\helpers\FileHelper;
class Widget extends Component class Widget extends Component
{ {
/** /**
* @var Widget|Controller the owner/creator of this widget. It could be either a widget or a controller. * @var View the view object that is used to create this widget.
* This property is automatically set by [[View::createWidget()]].
* This property is required by [[render()]] and [[renderFile()]].
*/ */
public $owner; public $view;
/** /**
* @var string id of the widget. * @var string id of the widget.
*/ */
@ -32,17 +34,6 @@ class Widget extends Component
private static $_counter = 0; private static $_counter = 0;
/** /**
* Constructor.
* @param Widget|Controller $owner owner/creator of this widget.
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($owner, $config = array())
{
$this->owner = $owner;
parent::__construct($config);
}
/**
* Returns the ID of the widget. * Returns the ID of the widget.
* @param boolean $autoGenerate whether to generate an ID if it is not set previously * @param boolean $autoGenerate whether to generate an ID if it is not set previously
* @return string ID of the widget. * @return string ID of the widget.
@ -73,6 +64,18 @@ class Widget extends Component
/** /**
* Renders a view. * Renders a view.
* The view to be rendered can be specified in one of the following formats:
*
* - path alias (e.g. "@app/views/site/index");
* - absolute path within application (e.g. "//site/index"): the view name starts with double slashes.
* The actual view file will be looked for under the [[Application::viewPath|view path]] of the application.
* - absolute path within module (e.g. "/site/index"): the view name starts with a single slash.
* The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently
* active module.
* - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]].
*
* If the view name does not contain a file extension, it will use the default one `.php`.
* @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name.
* @param array $params the parameters (name-value pairs) that should be made available in the view. * @param array $params the parameters (name-value pairs) that should be made available in the view.
* @return string the rendering result. * @return string the rendering result.
@ -80,7 +83,7 @@ class Widget extends Component
*/ */
public function render($view, $params = array()) public function render($view, $params = array())
{ {
return Yii::$app->getView()->render($view, $params, $this); return $this->view->render($view, $params, $this);
} }
/** /**
@ -92,7 +95,7 @@ class Widget extends Component
*/ */
public function renderFile($file, $params = array()) public function renderFile($file, $params = array())
{ {
return Yii::$app->getView()->renderFile($file, $params, $this); return $this->view->renderFile($file, $params, $this);
} }
/** /**
@ -106,4 +109,28 @@ class Widget extends Component
$class = new \ReflectionClass($className); $class = new \ReflectionClass($className);
return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views';
} }
/**
* Finds the view file based on the given view name.
* @param string $view the view name or the path alias of the view file. Please refer to [[render()]]
* on how to specify this parameter.
* @return string the view file path. Note that the file may not exist.
*/
protected function findViewFile($view)
{
if (strncmp($view, '@', 1) === 0) {
// e.g. "@app/views/main"
$file = Yii::getAlias($view);
} elseif (strncmp($view, '//', 2) === 0) {
// e.g. "//layouts/main"
$file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
} elseif (strncmp($view, '/', 1) === 0 && Yii::$app->controller !== null) {
// e.g. "/site/index"
$file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
} else {
$file = $this->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
}
return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file;
}
} }

1
framework/caching/ApcCache.php

@ -11,6 +11,7 @@ namespace yii\caching;
* ApcCache provides APC caching in terms of an application component. * ApcCache provides APC caching in terms of an application component.
* *
* To use this application component, the [APC PHP extension](http://www.php.net/apc) must be loaded. * To use this application component, the [APC PHP extension](http://www.php.net/apc) must be loaded.
* In order to enable APC for CLI you should add "apc.enable_cli = 1" to your php.ini.
* *
* See [[Cache]] for common cache operations that ApcCache supports. * See [[Cache]] for common cache operations that ApcCache supports.
* *

22
framework/caching/ChainedDependency.php

@ -22,11 +22,10 @@ namespace yii\caching;
class ChainedDependency extends Dependency class ChainedDependency extends Dependency
{ {
/** /**
* @var array list of dependencies that this dependency is composed of. * @var Dependency[] list of dependencies that this dependency is composed of.
* Each array element should be a dependency object or a configuration array * Each array element must be a dependency object.
* that can be used to create a dependency object via [[\Yii::createObject()]].
*/ */
public $dependencies = array(); public $dependencies;
/** /**
* @var boolean whether this dependency is depending on every dependency in [[dependencies]]. * @var boolean whether this dependency is depending on every dependency in [[dependencies]].
* Defaults to true, meaning if any of the dependencies has changed, this dependency is considered changed. * Defaults to true, meaning if any of the dependencies has changed, this dependency is considered changed.
@ -37,9 +36,8 @@ class ChainedDependency extends Dependency
/** /**
* Constructor. * Constructor.
* @param array $dependencies list of dependencies that this dependency is composed of. * @param Dependency[] $dependencies list of dependencies that this dependency is composed of.
* Each array element should be a dependency object or a configuration array * Each array element should be a dependency object.
* that can be used to create a dependency object via [[\Yii::createObject()]].
* @param array $config name-value pairs that will be used to initialize the object properties * @param array $config name-value pairs that will be used to initialize the object properties
*/ */
public function __construct($dependencies = array(), $config = array()) public function __construct($dependencies = array(), $config = array())
@ -54,10 +52,7 @@ class ChainedDependency extends Dependency
public function evaluateDependency() public function evaluateDependency()
{ {
foreach ($this->dependencies as $dependency) { foreach ($this->dependencies as $dependency) {
if (!$dependency instanceof Dependency) { $dependency->evaluateDependency();
$dependency = \Yii::createObject($dependency);
}
$dependency->evalulateDependency();
} }
} }
@ -79,10 +74,7 @@ class ChainedDependency extends Dependency
*/ */
public function getHasChanged() public function getHasChanged()
{ {
foreach ($this->dependencies as $i => $dependency) { foreach ($this->dependencies as $dependency) {
if (!$dependency instanceof Dependency) {
$this->dependencies[$i] = $dependency = \Yii::createObject($dependency);
}
if ($this->dependOnAll && $dependency->getHasChanged()) { if ($this->dependOnAll && $dependency->getHasChanged()) {
return true; return true;
} elseif (!$this->dependOnAll && !$dependency->getHasChanged()) { } elseif (!$this->dependOnAll && !$dependency->getHasChanged()) {

6
framework/caching/DbCache.php

@ -99,7 +99,7 @@ class DbCache extends Cache
$query = new Query; $query = new Query;
$query->select(array('data')) $query->select(array('data'))
->from($this->cacheTable) ->from($this->cacheTable)
->where('id = :id AND (expire = 0 OR expire >' . time() . ')', array(':id' => $key)); ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', array(':id' => $key));
if ($this->db->enableQueryCache) { if ($this->db->enableQueryCache) {
// temporarily disable and re-enable query caching // temporarily disable and re-enable query caching
$this->db->enableQueryCache = false; $this->db->enableQueryCache = false;
@ -125,7 +125,7 @@ class DbCache extends Cache
$query->select(array('id', 'data')) $query->select(array('id', 'data'))
->from($this->cacheTable) ->from($this->cacheTable)
->where(array('id' => $keys)) ->where(array('id' => $keys))
->andWhere('(expire = 0 OR expire > ' . time() . ')'); ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')');
if ($this->db->enableQueryCache) { if ($this->db->enableQueryCache) {
$this->db->enableQueryCache = false; $this->db->enableQueryCache = false;
@ -227,7 +227,7 @@ class DbCache extends Cache
{ {
if ($force || mt_rand(0, 1000000) < $this->gcProbability) { if ($force || mt_rand(0, 1000000) < $this->gcProbability) {
$this->db->createCommand() $this->db->createCommand()
->delete($this->cacheTable, 'expire > 0 AND expire < ' . time()) ->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time())
->execute(); ->execute();
} }
} }

1
framework/caching/DbDependency.php

@ -52,6 +52,7 @@ class DbDependency extends Dependency
/** /**
* Generates the data needed to determine if dependency has been changed. * Generates the data needed to determine if dependency has been changed.
* This method returns the value of the global state. * This method returns the value of the global state.
* @throws InvalidConfigException
* @return mixed the data needed to determine if dependency has been changed. * @return mixed the data needed to determine if dependency has been changed.
*/ */
protected function generateDependencyData() protected function generateDependencyData()

4
framework/caching/FileCache.php

@ -7,7 +7,7 @@
namespace yii\caching; namespace yii\caching;
use yii\base\InvalidConfigException; use Yii;
/** /**
* FileCache implements a cache component using files. * FileCache implements a cache component using files.
@ -51,7 +51,7 @@ class FileCache extends Cache
public function init() public function init()
{ {
parent::init(); parent::init();
$this->cachePath = \Yii::getAlias($this->cachePath); $this->cachePath = Yii::getAlias($this->cachePath);
if (!is_dir($this->cachePath)) { if (!is_dir($this->cachePath)) {
mkdir($this->cachePath, 0777, true); mkdir($this->cachePath, 0777, true);
} }

2
framework/caching/MemCache.php

@ -106,7 +106,7 @@ class MemCache extends Cache
/** /**
* Returns the underlying memcache (or memcached) object. * Returns the underlying memcache (or memcached) object.
* @return \Memcache|\Memcached the memcache (or memcached) object used by this cache component. * @return \Memcache|\Memcached the memcache (or memcached) object used by this cache component.
* @throws Exception if memcache or memcached extension is not loaded * @throws InvalidConfigException if memcache or memcached extension is not loaded
*/ */
public function getMemcache() public function getMemcache()
{ {

2
framework/caching/ZendDataCache.php

@ -10,7 +10,7 @@ namespace yii\caching;
/** /**
* ZendDataCache provides Zend data caching in terms of an application component. * ZendDataCache provides Zend data caching in terms of an application component.
* *
* To use this application component, the [Zend Data Cache PHP extensionn](http://www.zend.com/en/products/server/) * To use this application component, the [Zend Data Cache PHP extension](http://www.zend.com/en/products/server/)
* must be loaded. * must be loaded.
* *
* See [[Cache]] for common cache operations that ZendDataCache supports. * See [[Cache]] for common cache operations that ZendDataCache supports.

1
framework/console/Application.php

@ -129,6 +129,7 @@ class Application extends \yii\base\Application
'migrate' => 'yii\console\controllers\MigrateController', 'migrate' => 'yii\console\controllers\MigrateController',
'app' => 'yii\console\controllers\AppController', 'app' => 'yii\console\controllers\AppController',
'cache' => 'yii\console\controllers\CacheController', 'cache' => 'yii\console\controllers\CacheController',
'asset' => 'yii\console\controllers\AssetController',
); );
} }

1
framework/console/Controller.php

@ -24,7 +24,6 @@ use yii\base\InvalidRouteException;
* ~~~ * ~~~
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
*
* @since 2.0 * @since 2.0
*/ */
class Controller extends \yii\base\Controller class Controller extends \yii\base\Controller

123
framework/console/controllers/AppController.php

@ -86,7 +86,7 @@ class AppController extends Controller
$sourceDir = $this->getSourceDir(); $sourceDir = $this->getSourceDir();
$config = $this->getConfig(); $config = $this->getConfig();
$list = FileHelper::buildFileList($sourceDir, $path); $list = $this->buildFileList($sourceDir, $path);
if(is_array($config)) { if(is_array($config)) {
foreach($config as $file => $settings) { foreach($config as $file => $settings) {
@ -96,7 +96,7 @@ class AppController extends Controller
} }
} }
FileHelper::copyFiles($list); $this->copyFiles($list);
if(is_array($config)) { if(is_array($config)) {
foreach($config as $file => $settings) { foreach($config as $file => $settings) {
@ -159,7 +159,7 @@ class AppController extends Controller
* @param string $pathTo path to file we want to get relative path for * @param string $pathTo path to file we want to get relative path for
* @param string $varName variable name w/o $ to replace value with relative path for * @param string $varName variable name w/o $ to replace value with relative path for
* *
* @return string target file contetns * @return string target file contents
*/ */
public function replaceRelativePath($source, $pathTo, $varName) public function replaceRelativePath($source, $pathTo, $varName)
{ {
@ -204,4 +204,121 @@ class AppController extends Controller
return '__DIR__.\''.$up.'/'.basename($path1).'\''; return '__DIR__.\''.$up.'/'.basename($path1).'\'';
} }
/**
* Copies a list of files from one place to another.
* @param array $fileList the list of files to be copied (name=>spec).
* The array keys are names displayed during the copy process, and array values are specifications
* for files to be copied. Each array value must be an array of the following structure:
* <ul>
* <li>source: required, the full path of the file/directory to be copied from</li>
* <li>target: required, the full path of the file/directory to be copied to</li>
* <li>callback: optional, the callback to be invoked when copying a file. The callback function
* should be declared as follows:
* <pre>
* function foo($source,$params)
* </pre>
* where $source parameter is the source file path, and the content returned
* by the function will be saved into the target file.</li>
* <li>params: optional, the parameters to be passed to the callback</li>
* </ul>
* @see buildFileList
*/
protected function copyFiles($fileList)
{
$overwriteAll = false;
foreach($fileList as $name=>$file) {
$source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR);
$target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR);
$callback = isset($file['callback']) ? $file['callback'] : null;
$params = isset($file['params']) ? $file['params'] : null;
if(is_dir($source)) {
if (!is_dir($target)) {
mkdir($target, 0777, true);
}
continue;
}
if($callback !== null) {
$content = call_user_func($callback, $source, $params);
}
else {
$content = file_get_contents($source);
}
if(is_file($target)) {
if($content === file_get_contents($target)) {
echo " unchanged $name\n";
continue;
}
if($overwriteAll) {
echo " overwrite $name\n";
}
else {
echo " exist $name\n";
echo " ...overwrite? [Yes|No|All|Quit] ";
$answer = trim(fgets(STDIN));
if(!strncasecmp($answer, 'q', 1)) {
return;
}
elseif(!strncasecmp($answer, 'y', 1)) {
echo " overwrite $name\n";
}
elseif(!strncasecmp($answer, 'a', 1)) {
echo " overwrite $name\n";
$overwriteAll = true;
}
else {
echo " skip $name\n";
continue;
}
}
}
else {
if (!is_dir(dirname($target))) {
mkdir(dirname($target), 0777, true);
}
echo " generate $name\n";
}
file_put_contents($target, $content);
}
}
/**
* Builds the file list of a directory.
* This method traverses through the specified directory and builds
* a list of files and subdirectories that the directory contains.
* The result of this function can be passed to {@link copyFiles}.
* @param string $sourceDir the source directory
* @param string $targetDir the target directory
* @param string $baseDir base directory
* @param array $ignoreFiles list of the names of files that should
* be ignored in list building process.
* @param array $renameMap hash array of file names that should be
* renamed. Example value: array('1.old.txt'=>'2.new.txt').
* @return array the file list (see {@link copyFiles})
*/
protected function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array())
{
$list = array();
$handle = opendir($sourceDir);
while(($file = readdir($handle)) !== false) {
if(in_array($file, array('.', '..', '.svn', '.gitignore', '.hgignore')) || in_array($file, $ignoreFiles)) {
continue;
}
$sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file;
$targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap);
$name = $baseDir === '' ? $file : $baseDir.'/'.$file;
$list[$name] = array(
'source' => $sourcePath,
'target' => $targetPath,
);
if(is_dir($sourcePath)) {
$list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap));
}
}
closedir($handle);
return $list;
}
} }

353
framework/console/controllers/AssetController.php

@ -0,0 +1,353 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console\controllers;
use Yii;
use yii\console\Exception;
use yii\console\Controller;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class AssetController extends Controller
{
public $defaultAction = 'compress';
public $bundles = array();
public $extensions = array();
/**
* @var array
* ~~~
* 'all' => array(
* 'css' => 'all.css',
* 'js' => 'js.css',
* 'depends' => array( ... ),
* )
* ~~~
*/
public $targets = array();
public $assetManager = array();
public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}';
public $cssCompressor = 'java -jar yuicompressor.jar {from} -o {to}';
public function actionCompress($configFile, $bundleFile)
{
$this->loadConfiguration($configFile);
$bundles = $this->loadBundles($this->bundles, $this->extensions);
$targets = $this->loadTargets($this->targets, $bundles);
$this->publishBundles($bundles, $this->publishOptions);
$timestamp = time();
foreach ($targets as $target) {
if (!empty($target->js)) {
$this->buildTarget($target, 'js', $bundles, $timestamp);
}
if (!empty($target->css)) {
$this->buildTarget($target, 'css', $bundles, $timestamp);
}
}
$targets = $this->adjustDependency($targets, $bundles);
$this->saveTargets($targets, $bundleFile);
}
protected function loadConfiguration($configFile)
{
foreach (require($configFile) as $name => $value) {
if (property_exists($this, $name)) {
$this->$name = $value;
} else {
throw new Exception("Unknown configuration option: $name");
}
}
if (!isset($this->assetManager['basePath'])) {
throw new Exception("Please specify 'basePath' for the 'assetManager' option.");
}
if (!isset($this->assetManager['baseUrl'])) {
throw new Exception("Please specify 'baseUrl' for the 'assetManager' option.");
}
}
protected function loadBundles($bundles, $extensions)
{
$result = array();
foreach ($bundles as $name => $bundle) {
$bundle['class'] = 'yii\\web\\AssetBundle';
$result[$name] = Yii::createObject($bundle);
}
foreach ($extensions as $path) {
$manifest = $path . '/assets.php';
if (!is_file($manifest)) {
continue;
}
foreach (require($manifest) as $name => $bundle) {
if (!isset($result[$name])) {
$bundle['class'] = 'yii\\web\\AssetBundle';
$result[$name] = Yii::createObject($bundle);
}
}
}
return $result;
}
protected function loadTargets($targets, $bundles)
{
// build the dependency order of bundles
$registered = array();
foreach ($bundles as $name => $bundle) {
$this->registerBundle($bundles, $name, $registered);
}
$bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1));
// fill up the target which has empty 'depends'.
$referenced = array();
foreach ($targets as $name => $target) {
if (empty($target['depends'])) {
if (!isset($all)) {
$all = $name;
} else {
throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name");
}
} else {
foreach ($target['depends'] as $bundle) {
if (!isset($referenced[$bundle])) {
$referenced[$bundle] = $name;
} else {
throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time.");
}
}
}
}
if (isset($all)) {
$targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced));
}
// adjust the 'depends' order for each target according to the dependency order of bundles
// create an AssetBundle object for each target
foreach ($targets as $name => $target) {
if (!isset($target['basePath'])) {
throw new Exception("Please specify 'basePath' for the '$name' target.");
}
if (!isset($target['baseUrl'])) {
throw new Exception("Please specify 'baseUrl' for the '$name' target.");
}
usort($target['depends'], function ($a, $b) use ($bundleOrders) {
if ($bundleOrders[$a] == $bundleOrders[$b]) {
return 0;
} else {
return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1;
}
});
$target['class'] = 'yii\\web\\AssetBundle';
$targets[$name] = Yii::createObject($target);
}
return $targets;
}
/**
* @param \yii\web\AssetBundle[] $bundles
* @param array $options
*/
protected function publishBundles($bundles, $options)
{
if (!isset($options['class'])) {
$options['class'] = 'yii\\web\\AssetManager';
}
$am = Yii::createObject($options);
foreach ($bundles as $bundle) {
$bundle->publish($am);
}
}
/**
* @param \yii\web\AssetBundle $target
* @param string $type either "js" or "css"
* @param \yii\web\AssetBundle[] $bundles
* @param integer $timestamp
* @throws Exception
*/
protected function buildTarget($target, $type, $bundles, $timestamp)
{
$outputFile = strtr($target->$type, array(
'{ts}' => $timestamp,
));
$inputFiles = array();
foreach ($target->depends as $name) {
if (isset($bundles[$name])) {
foreach ($bundles[$name]->$type as $file) {
$inputFiles[] = $bundles[$name]->basePath . '/' . $file;
}
} else {
throw new Exception("Unknown bundle: $name");
}
}
if ($type === 'js') {
$this->compressJsFiles($inputFiles, $target->basePath . '/' . $outputFile);
} else {
$this->compressCssFiles($inputFiles, $target->basePath . '/' . $outputFile);
}
$target->$type = array($outputFile);
}
protected function adjustDependency($targets, $bundles)
{
$map = array();
foreach ($targets as $name => $target) {
foreach ($target->depends as $bundle) {
$map[$bundle] = $name;
}
}
foreach ($targets as $name => $target) {
$depends = array();
foreach ($target->depends as $bn) {
foreach ($bundles[$bn]->depends as $bundle) {
$depends[$map[$bundle]] = true;
}
}
unset($depends[$name]);
$target->depends = array_keys($depends);
}
// detect possible circular dependencies
foreach ($targets as $name => $target) {
$registered = array();
$this->registerBundle($targets, $name, $registered);
}
foreach ($map as $bundle => $target) {
$targets[$bundle] = Yii::createObject(array(
'class' => 'yii\\web\\AssetBundle',
'depends' => array($target),
));
}
return $targets;
}
protected function registerBundle($bundles, $name, &$registered)
{
if (!isset($registered[$name])) {
$registered[$name] = false;
$bundle = $bundles[$name];
foreach ($bundle->depends as $depend) {
$this->registerBundle($bundles, $depend, $registered);
}
unset($registered[$name]);
$registered[$name] = true;
} elseif ($registered[$name] === false) {
throw new Exception("A circular dependency is detected for target '$name'.");
}
}
protected function saveTargets($targets, $bundleFile)
{
$array = array();
foreach ($targets as $name => $target) {
foreach (array('js', 'css', 'depends', 'basePath', 'baseUrl') as $prop) {
if (!empty($target->$prop)) {
$array[$name][$prop] = $target->$prop;
}
}
}
$array = var_export($array, true);
$version = date('Y-m-d H:i:s', time());
file_put_contents($bundleFile, <<<EOD
<?php
/**
* This file is generated by the "yiic script" command.
* DO NOT MODIFY THIS FILE DIRECTLY.
* @version $version
*/
return $array;
EOD
);
}
protected function compressJsFiles($inputFiles, $outputFile)
{
if (is_string($this->jsCompressor)) {
$tmpFile = $outputFile . '.tmp';
$this->combineJsFiles($inputFiles, $tmpFile);
$log = shell_exec(strtr($this->jsCompressor, array(
'{from}' => $tmpFile,
'{to}' => $outputFile,
)));
@unlink($tmpFile);
} else {
$log = call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile);
}
}
protected function compressCssFiles($inputFiles, $outputFile)
{
if (is_string($this->cssCompressor)) {
$tmpFile = $outputFile . '.tmp';
$this->combineCssFiles($inputFiles, $tmpFile);
$log = shell_exec(strtr($this->cssCompressor, array(
'{from}' => $inputFiles,
'{to}' => $outputFile,
)));
} else {
$log = call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile);
}
}
public function combineJsFiles($files, $tmpFile)
{
$content = '';
foreach ($files as $file) {
$content .= "/*** BEGIN FILE: $file ***/\n"
. file_get_contents($file)
. "/*** END FILE: $file ***/\n";
}
file_put_contents($tmpFile, $content);
}
public function combineCssFiles($files, $tmpFile)
{
// todo: adjust url() references in CSS files
$content = '';
foreach ($files as $file) {
$content .= "/*** BEGIN FILE: $file ***/\n"
. file_get_contents($file)
. "/*** END FILE: $file ***/\n";
}
file_put_contents($tmpFile, $content);
}
public function actionTemplate($configFile)
{
$template = <<<EOD
<?php
return array(
//
'bundles' => require('path/to/bundles.php'),
//
'extensions' => require('path/to/namespaces.php'),
//
'targets' => array(
'all' => array(
'basePath' => __DIR__,
'baseUrl' => '/test',
'js' => 'all-{ts}.js',
'css' => 'all-{ts}.css',
),
),
'assetManager' => array(
'basePath' => __DIR__,
'baseUrl' => '/test',
),
);
EOD;
file_put_contents($configFile, $template);
}
}

4
framework/console/controllers/HelpController.php

@ -9,9 +9,9 @@ namespace yii\console\controllers;
use Yii; use Yii;
use yii\base\Application; use yii\base\Application;
use yii\console\Exception;
use yii\base\InlineAction; use yii\base\InlineAction;
use yii\console\Controller; use yii\console\Controller;
use yii\console\Exception;
use yii\console\Request; use yii\console\Request;
use yii\helpers\StringHelper; use yii\helpers\StringHelper;
@ -128,7 +128,7 @@ class HelpController extends Controller
$files = scandir($module->getControllerPath()); $files = scandir($module->getControllerPath());
foreach ($files as $file) { foreach ($files as $file) {
if(strcmp(substr($file,-14),'Controller.php') === 0 && is_file($file)) { if (strcmp(substr($file, -14), 'Controller.php') === 0) {
$commands[] = $prefix . lcfirst(substr(basename($file), 0, -14)); $commands[] = $prefix . lcfirst(substr(basename($file), 0, -14));
} }
} }

8
framework/console/webapp/default/index.php

@ -1,10 +1,10 @@
<?php <?php
define('YII_DEBUG', true); define('YII_DEBUG', true);
$yii = __DIR__.'/../framework/yii.php'; require __DIR__.'/../framework/yii.php';
require $yii;
$config = require dirname(__DIR__).'/protected/config/main.php'; $config = require dirname(__DIR__).'/protected/config/main.php';
$config['basePath'] = dirname(__DIR__).'/protected';
$basePath = dirname(__DIR__).'/protected'; $app = new \yii\web\Application($config);
$app = new \yii\web\Application('webapp', $basePath, $config);
$app->run(); $app->run();

4
framework/console/webapp/default/protected/config/main.php

@ -1,5 +1,6 @@
<?php <?php
return array( return array(
'id' => 'webapp',
'name' => 'My Web Application', 'name' => 'My Web Application',
'components' => array( 'components' => array(
@ -12,5 +13,8 @@ return array(
'password' => '', 'password' => '',
), ),
*/ */
'cache' => array(
'class' => 'yii\caching\DummyCache',
),
), ),
); );

7
framework/console/webapp/default/protected/views/layouts/main.php

@ -1,11 +1,12 @@
<?php use yii\helpers\Html as Html; ?>
<!doctype html> <!doctype html>
<html lang="en"> <html lang="<?php \Yii::$app->language?>">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title><?php echo $this->context->pageTitle?></title> <title><?php echo Html::encode($this->title)?></title>
</head> </head>
<body> <body>
<h1><?php echo $this->context->pageTitle?></h1> <h1><?php echo Html::encode($this->title)?></h1>
<div class="content"> <div class="content">
<?php echo $content?> <?php echo $content?>
</div> </div>

99
framework/db/ActiveRecord.php

@ -191,15 +191,12 @@ class ActiveRecord extends Model
*/ */
public static function updateAllCounters($counters, $condition = '', $params = array()) public static function updateAllCounters($counters, $condition = '', $params = array())
{ {
$db = static::getDb();
$n = 0; $n = 0;
foreach ($counters as $name => $value) { foreach ($counters as $name => $value) {
$quotedName = $db->quoteColumnName($name); $counters[$name] = new Expression("[[$name]]+:bp{$n}", array(":bp{$n}" => $value));
$counters[$name] = new Expression("$quotedName+:bp{$n}");
$params[":bp{$n}"] = $value;
$n++; $n++;
} }
$command = $db->createCommand(); $command = static::getDb()->createCommand();
$command->update(static::tableName(), $counters, $condition, $params); $command->update(static::tableName(), $counters, $condition, $params);
return $command->execute(); return $command->execute();
} }
@ -280,6 +277,34 @@ class ActiveRecord extends Model
} }
/** /**
* Returns the name of the column that stores the lock version for implementing optimistic locking.
*
* Optimistic locking allows multiple users to access the same record for edits and avoids
* potential conflicts. In case when a user attempts to save the record upon some staled data
* (because another user has modified the data), a [[StaleObjectException]] exception will be thrown,
* and the update or deletion is skipped.
*
* Optimized locking is only supported by [[update()]] and [[delete()]].
*
* To use optimized locking:
*
* 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
* Override this method to return the name of this column.
* 2. In the Web form that collects the user input, add a hidden field that stores
* the lock version of the recording being updated.
* 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]]
* and implement necessary business logic (e.g. merging the changes, prompting stated data)
* to resolve the conflict.
*
* @return string the column name that stores the lock version of a table row.
* If null is returned (default implemented), optimistic locking will not be supported.
*/
public function optimisticLock()
{
return null;
}
/**
* PHP getter magic method. * PHP getter magic method.
* This method is overridden so that attributes and related objects can be accessed like properties. * This method is overridden so that attributes and related objects can be accessed like properties.
* @param string $name property name * @param string $name property name
@ -530,8 +555,8 @@ class ActiveRecord extends Model
*/ */
public function isAttributeChanged($name) public function isAttributeChanged($name)
{ {
if (isset($this->_attribute[$name], $this->_oldAttributes[$name])) { if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
return $this->_attribute[$name] !== $this->_oldAttributes[$name]; return $this->_attributes[$name] !== $this->_oldAttributes[$name];
} else { } else {
return isset($this->_attributes[$name]) || isset($this->_oldAttributes); return isset($this->_attributes[$name]) || isset($this->_oldAttributes);
} }
@ -590,7 +615,11 @@ class ActiveRecord extends Model
*/ */
public function save($runValidation = true, $attributes = null) public function save($runValidation = true, $attributes = null)
{ {
return $this->getIsNewRecord() ? $this->insert($runValidation, $attributes) : $this->update($runValidation, $attributes); if ($this->getIsNewRecord()) {
return $this->insert($runValidation, $attributes);
} else {
return $this->update($runValidation, $attributes) !== false;
}
} }
/** /**
@ -692,11 +721,26 @@ class ActiveRecord extends Model
* $customer->update(); * $customer->update();
* ~~~ * ~~~
* *
* Note that it is possible the update does not affect any row in the table.
* In this case, this method will return 0. For this reason, you should use the following
* code to check if update() is successful or not:
*
* ~~~
* if ($this->update() !== false) {
* // update successful
* } else {
* // update failed
* }
* ~~~
*
* @param boolean $runValidation whether to perform validation before saving the record. * @param boolean $runValidation whether to perform validation before saving the record.
* If the validation fails, the record will not be inserted into the database. * If the validation fails, the record will not be inserted into the database.
* @param array $attributes list of attributes that need to be saved. Defaults to null, * @param array $attributes list of attributes that need to be saved. Defaults to null,
* meaning all attributes that are loaded from DB will be saved. * meaning all attributes that are loaded from DB will be saved.
* @return boolean whether the attributes are valid and the record is updated successfully. * @return integer|boolean the number of rows affected, or false if validation fails
* or [[beforeSave()]] stops the updating process.
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
* being updated is outdated.
*/ */
public function update($runValidation = true, $attributes = null) public function update($runValidation = true, $attributes = null)
{ {
@ -706,15 +750,31 @@ class ActiveRecord extends Model
if ($this->beforeSave(false)) { if ($this->beforeSave(false)) {
$values = $this->getDirtyAttributes($attributes); $values = $this->getDirtyAttributes($attributes);
if ($values !== array()) { if ($values !== array()) {
$condition = $this->getOldPrimaryKey(true);
$lock = $this->optimisticLock();
if ($lock !== null) {
if (!isset($values[$lock])) {
$values[$lock] = $this->$lock + 1;
}
$condition[$lock] = $this->$lock;
}
// We do not check the return value of updateAll() because it's possible // We do not check the return value of updateAll() because it's possible
// that the UPDATE statement doesn't change anything and thus returns 0. // that the UPDATE statement doesn't change anything and thus returns 0.
$this->updateAll($values, $this->getOldPrimaryKey(true)); $rows = $this->updateAll($values, $condition);
if ($lock !== null && !$rows) {
throw new StaleObjectException('The object being updated is outdated.');
}
foreach ($values as $name => $value) { foreach ($values as $name => $value) {
$this->_oldAttributes[$name] = $this->_attributes[$name]; $this->_oldAttributes[$name] = $this->_attributes[$name];
} }
$this->afterSave(false); $this->afterSave(false);
return $rows;
} else {
return 0;
} }
return true;
} else { } else {
return false; return false;
} }
@ -763,17 +823,28 @@ class ActiveRecord extends Model
* In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
* will be raised by the corresponding methods. * will be raised by the corresponding methods.
* *
* @return boolean whether the deletion is successful. * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason.
* Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
* being deleted is outdated.
*/ */
public function delete() public function delete()
{ {
if ($this->beforeDelete()) { if ($this->beforeDelete()) {
// we do not check the return value of deleteAll() because it's possible // we do not check the return value of deleteAll() because it's possible
// the record is already deleted in the database and thus the method will return 0 // the record is already deleted in the database and thus the method will return 0
$this->deleteAll($this->getPrimaryKey(true)); $condition = $this->getOldPrimaryKey(true);
$lock = $this->optimisticLock();
if ($lock !== null) {
$condition[$lock] = $this->$lock;
}
$rows = $this->deleteAll($condition);
if ($lock !== null && !$rows) {
throw new StaleObjectException('The object being deleted is outdated.');
}
$this->_oldAttributes = null; $this->_oldAttributes = null;
$this->afterDelete(); $this->afterDelete();
return true; return $rows;
} else { } else {
return false; return false;
} }

90
framework/db/Command.php

@ -84,39 +84,51 @@ class Command extends \yii\base\Component
/** /**
* Specifies the SQL statement to be executed. * Specifies the SQL statement to be executed.
* Any previous execution will be terminated or cancelled. * The previous SQL execution (if any) will be cancelled, and [[params]] will be cleared as well.
* @param string $sql the SQL statement to be set. * @param string $sql the SQL statement to be set.
* @return Command this command instance * @return Command this command instance
*/ */
public function setSql($sql) public function setSql($sql)
{ {
if ($sql !== $this->_sql) { if ($sql !== $this->_sql) {
if ($this->db->enableAutoQuoting && $sql != '') {
$sql = $this->expandSql($sql);
}
$this->cancel(); $this->cancel();
$this->_sql = $sql; $this->_sql = $this->db->quoteSql($sql);
$this->_params = array(); $this->_params = array();
} }
return $this; return $this;
} }
/** /**
* Expands a SQL statement by quoting table and column names and replacing table prefixes. * Returns the raw SQL by inserting parameter values into the corresponding placeholders in [[sql]].
* @param string $sql the SQL to be expanded * Note that the return value of this method should mainly be used for logging purpose.
* @return string the expanded SQL * It is likely that this method returns an invalid SQL due to improper replacement of parameter placeholders.
* @return string the raw SQL
*/ */
protected function expandSql($sql) public function getRawSql()
{ {
$db = $this->db; if ($this->_params === array()) {
return preg_replace_callback('/(\\{\\{(.*?)\\}\\}|\\[\\[(.*?)\\]\\])/', function($matches) use($db) { return $this->_sql;
if (isset($matches[3])) {
return $db->quoteColumnName($matches[3]);
} else { } else {
$name = str_replace('%', $db->tablePrefix, $matches[2]); $params = array();
return $db->quoteTableName($name); foreach ($this->_params as $name => $value) {
if (is_string($value)) {
$params[$name] = $this->db->quoteValue($value);
} elseif ($value === null) {
$params[$name] = 'NULL';
} else {
$params[$name] = $value;
}
}
if (isset($params[1])) {
$sql = '';
foreach (explode('?', $this->_sql) as $i => $part) {
$sql .= (isset($params[$i]) ? $params[$i] : '') . $part;
}
return $sql;
} else {
return strtr($this->_sql, $params);
}
} }
}, $sql);
} }
/** /**
@ -134,7 +146,7 @@ class Command extends \yii\base\Component
try { try {
$this->pdoStatement = $this->db->pdo->prepare($sql); $this->pdoStatement = $this->db->pdo->prepare($sql);
} catch (\Exception $e) { } catch (\Exception $e) {
Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __CLASS__); Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __METHOD__);
$errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode()); throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode());
} }
@ -243,6 +255,7 @@ class Command extends \yii\base\Component
'boolean' => \PDO::PARAM_BOOL, 'boolean' => \PDO::PARAM_BOOL,
'integer' => \PDO::PARAM_INT, 'integer' => \PDO::PARAM_INT,
'string' => \PDO::PARAM_STR, 'string' => \PDO::PARAM_STR,
'resource' => \PDO::PARAM_LOB,
'NULL' => \PDO::PARAM_NULL, 'NULL' => \PDO::PARAM_NULL,
); );
$type = gettype($data); $type = gettype($data);
@ -260,21 +273,18 @@ class Command extends \yii\base\Component
{ {
$sql = $this->getSql(); $sql = $this->getSql();
if ($this->_params === array()) { $rawSql = $this->getRawSql();
$paramLog = '';
} else {
$paramLog = "\nParameters: " . var_export($this->_params, true);
}
Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); Yii::trace("Executing SQL: $rawSql", __METHOD__);
if ($sql == '') { if ($sql == '') {
return 0; return 0;
} }
try { try {
$token = "SQL: $sql";
if ($this->db->enableProfiling) { if ($this->db->enableProfiling) {
Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); Yii::beginProfile($token, __METHOD__);
} }
$this->prepare(); $this->prepare();
@ -282,16 +292,16 @@ class Command extends \yii\base\Component
$n = $this->pdoStatement->rowCount(); $n = $this->pdoStatement->rowCount();
if ($this->db->enableProfiling) { if ($this->db->enableProfiling) {
Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); Yii::endProfile($token, __METHOD__);
} }
return $n; return $n;
} catch (\Exception $e) { } catch (\Exception $e) {
if ($this->db->enableProfiling) { if ($this->db->enableProfiling) {
Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); Yii::endProfile($token, __METHOD__);
} }
$message = $e->getMessage(); $message = $e->getMessage();
Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); Yii::error("$message\nFailed to execute SQL: $rawSql", __METHOD__);
$errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
throw new Exception($message, $errorInfo, (int)$e->getCode()); throw new Exception($message, $errorInfo, (int)$e->getCode());
@ -377,13 +387,9 @@ class Command extends \yii\base\Component
{ {
$db = $this->db; $db = $this->db;
$sql = $this->getSql(); $sql = $this->getSql();
if ($this->_params === array()) { $rawSql = $this->getRawSql();
$paramLog = '';
} else {
$paramLog = "\nParameters: " . var_export($this->_params, true);
}
Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); Yii::trace("Querying SQL: $rawSql", __METHOD__);
/** @var $cache \yii\caching\Cache */ /** @var $cache \yii\caching\Cache */
if ($db->enableQueryCache && $method !== '') { if ($db->enableQueryCache && $method !== '') {
@ -395,18 +401,18 @@ class Command extends \yii\base\Component
__CLASS__, __CLASS__,
$db->dsn, $db->dsn,
$db->username, $db->username,
$sql, $rawSql,
$paramLog,
)); ));
if (($result = $cache->get($cacheKey)) !== false) { if (($result = $cache->get($cacheKey)) !== false) {
Yii::trace('Query result served from cache', __CLASS__); Yii::trace('Query result served from cache', __METHOD__);
return $result; return $result;
} }
} }
try { try {
$token = "SQL: $sql";
if ($db->enableProfiling) { if ($db->enableProfiling) {
Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); Yii::beginProfile($token, __METHOD__);
} }
$this->prepare(); $this->prepare();
@ -423,21 +429,21 @@ class Command extends \yii\base\Component
} }
if ($db->enableProfiling) { if ($db->enableProfiling) {
Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); Yii::endProfile($token, __METHOD__);
} }
if (isset($cache, $cacheKey) && $cache instanceof Cache) { if (isset($cache, $cacheKey) && $cache instanceof Cache) {
$cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency);
Yii::trace('Saved query result in cache', __CLASS__); Yii::trace('Saved query result in cache', __METHOD__);
} }
return $result; return $result;
} catch (\Exception $e) { } catch (\Exception $e) {
if ($db->enableProfiling) { if ($db->enableProfiling) {
Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); Yii::endProfile($token, __METHOD__);
} }
$message = $e->getMessage(); $message = $e->getMessage();
Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); Yii::error("$message\nCommand::$method() failed: $rawSql", __METHOD__);
$errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
throw new Exception($message, $errorInfo, (int)$e->getCode()); throw new Exception($message, $errorInfo, (int)$e->getCode());
} }
@ -541,7 +547,7 @@ class Command extends \yii\base\Component
*/ */
public function delete($table, $condition = '', $params = array()) public function delete($table, $condition = '', $params = array())
{ {
$sql = $this->db->getQueryBuilder()->delete($table, $condition); $sql = $this->db->getQueryBuilder()->delete($table, $condition, $params);
return $this->setSql($sql)->bindValues($params); return $this->setSql($sql)->bindValues($params);
} }

42
framework/db/Connection.php

@ -223,21 +223,10 @@ class Connection extends Component
* @var string the common prefix or suffix for table names. If a table name is given * @var string the common prefix or suffix for table names. If a table name is given
* as `{{%TableName}}`, then the percentage character `%` will be replaced with this * as `{{%TableName}}`, then the percentage character `%` will be replaced with this
* property value. For example, `{{%post}}` becomes `{{tbl_post}}` if this property is * property value. For example, `{{%post}}` becomes `{{tbl_post}}` if this property is
* set as `"tbl_"`. Note that this property is only effective when [[enableAutoQuoting]] * set as `"tbl_"`.
* is true.
* @see enableAutoQuoting
*/ */
public $tablePrefix; public $tablePrefix;
/** /**
* @var boolean whether to enable automatic quoting of table names and column names.
* Defaults to true. When this property is true, any token enclosed within double curly brackets
* (e.g. `{{post}}`) in a SQL statement will be treated as a table name and will be quoted
* accordingly when the SQL statement is executed; and any token enclosed within double square
* brackets (e.g. `[[name]]`) will be treated as a column name and quoted accordingly.
* @see tablePrefix
*/
public $enableAutoQuoting = true;
/**
* @var array mapping between PDO driver names and [[Schema]] classes. * @var array mapping between PDO driver names and [[Schema]] classes.
* The keys of the array are PDO driver names while the values the corresponding * The keys of the array are PDO driver names while the values the corresponding
* schema class name or configuration. Please refer to [[\Yii::createObject()]] for * schema class name or configuration. Please refer to [[\Yii::createObject()]] for
@ -254,9 +243,9 @@ class Connection extends Component
'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3
'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2 'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2
'mssql' => 'yi\db\dao\mssql\Schema', // Mssql driver on windows hosts 'mssql' => 'yi\db\dao\mssql\Schema', // Mssql driver on windows hosts
'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts
'sqlsrv' => 'yii\db\mssql\Schema', // Mssql 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql
'oci' => 'yii\db\oci\Schema', // Oracle driver 'oci' => 'yii\db\oci\Schema', // Oracle driver
'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts
); );
/** /**
* @var Transaction the currently active transaction * @var Transaction the currently active transaction
@ -324,12 +313,12 @@ class Connection extends Component
throw new InvalidConfigException('Connection::dsn cannot be empty.'); throw new InvalidConfigException('Connection::dsn cannot be empty.');
} }
try { try {
\Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); \Yii::trace('Opening DB connection: ' . $this->dsn, __METHOD__);
$this->pdo = $this->createPdoInstance(); $this->pdo = $this->createPdoInstance();
$this->initConnection(); $this->initConnection();
} }
catch (\PDOException $e) { catch (\PDOException $e) {
\Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __CLASS__); \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __METHOD__);
$message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; $message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.';
throw new Exception($message, $e->errorInfo, (int)$e->getCode()); throw new Exception($message, $e->errorInfo, (int)$e->getCode());
} }
@ -343,7 +332,7 @@ class Connection extends Component
public function close() public function close()
{ {
if ($this->pdo !== null) { if ($this->pdo !== null) {
\Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); \Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__);
$this->pdo = null; $this->pdo = null;
$this->_schema = null; $this->_schema = null;
$this->_transaction = null; $this->_transaction = null;
@ -518,6 +507,27 @@ class Connection extends Component
} }
/** /**
* Processes a SQL statement by quoting table and column names that are enclosed within double brackets.
* Tokens enclosed within double curly brackets are treated as table names, while
* tokens enclosed within double square brackets are column names. They will be quoted accordingly.
* Also, the percentage character "%" in a table name will be replaced with [[tablePrefix]].
* @param string $sql the SQL to be quoted
* @return string the quoted SQL
*/
public function quoteSql($sql)
{
$db = $this;
return preg_replace_callback('/(\\{\\{([\w\-\. ]+)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/',
function($matches) use($db) {
if (isset($matches[3])) {
return $db->quoteColumnName($matches[3]);
} else {
return str_replace('%', $db->tablePrefix, $db->quoteTableName($matches[2]));
}
}, $sql);
}
/**
* Returns the name of the DB driver for the current [[dsn]]. * Returns the name of the DB driver for the current [[dsn]].
* @return string name of the DB driver * @return string name of the DB driver
*/ */

479
framework/db/QueryBuilder.php

@ -22,6 +22,11 @@ use yii\base\NotSupportedException;
class QueryBuilder extends \yii\base\Object class QueryBuilder extends \yii\base\Object
{ {
/** /**
* The prefix for automatically generated query binding parameters.
*/
const PARAM_PREFIX = ':qp';
/**
* @var Connection the database connection. * @var Connection the database connection.
*/ */
public $db; public $db;
@ -58,11 +63,11 @@ class QueryBuilder extends \yii\base\Object
$clauses = array( $clauses = array(
$this->buildSelect($query->select, $query->distinct, $query->selectOption), $this->buildSelect($query->select, $query->distinct, $query->selectOption),
$this->buildFrom($query->from), $this->buildFrom($query->from),
$this->buildJoin($query->join), $this->buildJoin($query->join, $query->params),
$this->buildWhere($query->where), $this->buildWhere($query->where, $query->params),
$this->buildGroupBy($query->groupBy), $this->buildGroupBy($query->groupBy),
$this->buildHaving($query->having), $this->buildHaving($query->having, $query->params),
$this->buildUnion($query->union), $this->buildUnion($query->union, $query->params),
$this->buildOrderBy($query->orderBy), $this->buildOrderBy($query->orderBy),
$this->buildLimit($query->limit, $query->offset), $this->buildLimit($query->limit, $query->offset),
); );
@ -92,7 +97,6 @@ class QueryBuilder extends \yii\base\Object
{ {
$names = array(); $names = array();
$placeholders = array(); $placeholders = array();
$count = 0;
foreach ($columns as $name => $value) { foreach ($columns as $name => $value) {
$names[] = $this->db->quoteColumnName($name); $names[] = $this->db->quoteColumnName($name);
if ($value instanceof Expression) { if ($value instanceof Expression) {
@ -101,9 +105,9 @@ class QueryBuilder extends \yii\base\Object
$params[$n] = $v; $params[$n] = $v;
} }
} else { } else {
$placeholders[] = ':p' . $count; $phName = self::PARAM_PREFIX . count($params);
$params[':p' . $count] = $value; $placeholders[] = $phName;
$count++; $params[$phName] = $value;
} }
} }
@ -159,10 +163,9 @@ class QueryBuilder extends \yii\base\Object
* so that they can be bound to the DB command later. * so that they can be bound to the DB command later.
* @return string the UPDATE SQL * @return string the UPDATE SQL
*/ */
public function update($table, $columns, $condition = '', &$params) public function update($table, $columns, $condition, &$params)
{ {
$lines = array(); $lines = array();
$count = 0;
foreach ($columns as $name => $value) { foreach ($columns as $name => $value) {
if ($value instanceof Expression) { if ($value instanceof Expression) {
$lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression; $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression;
@ -170,17 +173,15 @@ class QueryBuilder extends \yii\base\Object
$params[$n] = $v; $params[$n] = $v;
} }
} else { } else {
$lines[] = $this->db->quoteColumnName($name) . '=:p' . $count; $phName = self::PARAM_PREFIX . count($params);
$params[':p' . $count] = $value; $lines[] = $this->db->quoteColumnName($name) . '=' . $phName;
$count++; $params[$phName] = $value;
} }
} }
$sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
if (($where = $this->buildCondition($condition)) !== '') {
$sql .= ' WHERE ' . $where;
}
return $sql; $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
$where = $this->buildWhere($condition, $params);
return $where === '' ? $sql : $sql . ' ' . $where;
} }
/** /**
@ -196,15 +197,15 @@ class QueryBuilder extends \yii\base\Object
* @param string $table the table where the data will be deleted from. * @param string $table the table where the data will be deleted from.
* @param mixed $condition the condition that will be put in the WHERE part. Please * @param mixed $condition the condition that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify condition. * refer to [[Query::where()]] on how to specify condition.
* @param array $params the binding parameters that will be modified by this method
* so that they can be bound to the DB command later.
* @return string the DELETE SQL * @return string the DELETE SQL
*/ */
public function delete($table, $condition = '') public function delete($table, $condition, &$params)
{ {
$sql = 'DELETE FROM ' . $this->db->quoteTableName($table); $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
if (($where = $this->buildCondition($condition)) !== '') { $where = $this->buildWhere($condition, $params);
$sql .= ' WHERE ' . $where; return $where === '' ? $sql : $sql . ' ' . $where;
}
return $sql;
} }
/** /**
@ -479,200 +480,6 @@ class QueryBuilder extends \yii\base\Object
} }
/** /**
* Parses the condition specification and generates the corresponding SQL expression.
* @param string|array $condition the condition specification. Please refer to [[Query::where()]]
* on how to specify a condition.
* @return string the generated SQL expression
* @throws \yii\db\Exception if the condition is in bad format
*/
public function buildCondition($condition)
{
static $builders = array(
'AND' => 'buildAndCondition',
'OR' => 'buildAndCondition',
'BETWEEN' => 'buildBetweenCondition',
'NOT BETWEEN' => 'buildBetweenCondition',
'IN' => 'buildInCondition',
'NOT IN' => 'buildInCondition',
'LIKE' => 'buildLikeCondition',
'NOT LIKE' => 'buildLikeCondition',
'OR LIKE' => 'buildLikeCondition',
'OR NOT LIKE' => 'buildLikeCondition',
);
if (!is_array($condition)) {
return (string)$condition;
} elseif ($condition === array()) {
return '';
}
if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
$operator = strtoupper($condition[0]);
if (isset($builders[$operator])) {
$method = $builders[$operator];
array_shift($condition);
return $this->$method($operator, $condition);
} else {
throw new Exception('Found unknown operator in query: ' . $operator);
}
} else { // hash format: 'column1'=>'value1', 'column2'=>'value2', ...
return $this->buildHashCondition($condition);
}
}
private function buildHashCondition($condition)
{
$parts = array();
foreach ($condition as $column => $value) {
if (is_array($value)) { // IN condition
$parts[] = $this->buildInCondition('in', array($column, $value));
} else {
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if ($value === null) {
$parts[] = "$column IS NULL";
} elseif (is_string($value)) {
$parts[] = "$column=" . $this->db->quoteValue($value);
} else {
$parts[] = "$column=$value";
}
}
}
return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
}
private function buildAndCondition($operator, $operands)
{
$parts = array();
foreach ($operands as $operand) {
if (is_array($operand)) {
$operand = $this->buildCondition($operand);
}
if ($operand !== '') {
$parts[] = $operand;
}
}
if ($parts !== array()) {
return '(' . implode(") $operator (", $parts) . ')';
} else {
return '';
}
}
private function buildBetweenCondition($operator, $operands)
{
if (!isset($operands[0], $operands[1], $operands[2])) {
throw new Exception("Operator '$operator' requires three operands.");
}
list($column, $value1, $value2) = $operands;
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$value1 = is_string($value1) ? $this->db->quoteValue($value1) : (string)$value1;
$value2 = is_string($value2) ? $this->db->quoteValue($value2) : (string)$value2;
return "$column $operator $value1 AND $value2";
}
private function buildInCondition($operator, $operands)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
$values = (array)$values;
if ($values === array() || $column === array()) {
return $operator === 'IN' ? '0=1' : '';
}
if (count($column) > 1) {
return $this->buildCompositeInCondition($operator, $column, $values);
} elseif (is_array($column)) {
$column = reset($column);
}
foreach ($values as $i => $value) {
if (is_array($value)) {
$value = isset($value[$column]) ? $value[$column] : null;
}
if ($value === null) {
$values[$i] = 'NULL';
} else {
$values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value;
}
}
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if (count($values) > 1) {
return "$column $operator (" . implode(', ', $values) . ')';
} else {
$operator = $operator === 'IN' ? '=' : '<>';
return "$column$operator{$values[0]}";
}
}
protected function buildCompositeInCondition($operator, $columns, $values)
{
foreach ($columns as $i => $column) {
if (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
}
}
$vss = array();
foreach ($values as $value) {
$vs = array();
foreach ($columns as $column) {
if (isset($value[$column])) {
$vs[] = is_string($value[$column]) ? $this->db->quoteValue($value[$column]) : (string)$value[$column];
} else {
$vs[] = 'NULL';
}
}
$vss[] = '(' . implode(', ', $vs) . ')';
}
return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')';
}
private function buildLikeCondition($operator, $operands)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
$values = (array)$values;
if ($values === array()) {
return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : '';
}
if ($operator === 'LIKE' || $operator === 'NOT LIKE') {
$andor = ' AND ';
} else {
$andor = ' OR ';
$operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE';
}
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$parts = array();
foreach ($values as $value) {
$parts[] = "$column $operator " . $this->db->quoteValue($value);
}
return implode($andor, $parts);
}
/**
* @param array $columns * @param array $columns
* @param boolean $distinct * @param boolean $distinct
* @param string $selectOption * @param string $selectOption
@ -737,10 +544,11 @@ class QueryBuilder extends \yii\base\Object
/** /**
* @param string|array $joins * @param string|array $joins
* @param array $params the binding parameters to be populated
* @return string the JOIN clause built from [[query]]. * @return string the JOIN clause built from [[query]].
* @throws Exception if the $joins parameter is not in proper format * @throws Exception if the $joins parameter is not in proper format
*/ */
public function buildJoin($joins) public function buildJoin($joins, &$params)
{ {
if (empty($joins)) { if (empty($joins)) {
return ''; return '';
@ -761,9 +569,9 @@ class QueryBuilder extends \yii\base\Object
} }
$joins[$i] = $join[0] . ' ' . $table; $joins[$i] = $join[0] . ' ' . $table;
if (isset($join[2])) { if (isset($join[2])) {
$condition = $this->buildCondition($join[2]); $condition = $this->buildCondition($join[2], $params);
if ($condition !== '') { if ($condition !== '') {
$joins[$i] .= ' ON ' . $this->buildCondition($join[2]); $joins[$i] .= ' ON ' . $condition;
} }
} }
} else { } else {
@ -776,11 +584,12 @@ class QueryBuilder extends \yii\base\Object
/** /**
* @param string|array $condition * @param string|array $condition
* @param array $params the binding parameters to be populated
* @return string the WHERE clause built from [[query]]. * @return string the WHERE clause built from [[query]].
*/ */
public function buildWhere($condition) public function buildWhere($condition, &$params)
{ {
$where = $this->buildCondition($condition); $where = $this->buildCondition($condition, $params);
return $where === '' ? '' : 'WHERE ' . $where; return $where === '' ? '' : 'WHERE ' . $where;
} }
@ -795,11 +604,12 @@ class QueryBuilder extends \yii\base\Object
/** /**
* @param string|array $condition * @param string|array $condition
* @param array $params the binding parameters to be populated
* @return string the HAVING clause built from [[query]]. * @return string the HAVING clause built from [[query]].
*/ */
public function buildHaving($condition) public function buildHaving($condition, &$params)
{ {
$having = $this->buildCondition($condition); $having = $this->buildCondition($condition, $params);
return $having === '' ? '' : 'HAVING ' . $having; return $having === '' ? '' : 'HAVING ' . $having;
} }
@ -843,16 +653,19 @@ class QueryBuilder extends \yii\base\Object
/** /**
* @param array $unions * @param array $unions
* @param array $params the binding parameters to be populated
* @return string the UNION clause built from [[query]]. * @return string the UNION clause built from [[query]].
*/ */
public function buildUnion($unions) public function buildUnion($unions, &$params)
{ {
if (empty($unions)) { if (empty($unions)) {
return ''; return '';
} }
foreach ($unions as $i => $union) { foreach ($unions as $i => $union) {
if ($union instanceof Query) { if ($union instanceof Query) {
$union->addParams($params);
$unions[$i] = $this->build($union); $unions[$i] = $this->build($union);
$params = $union->params;
} }
} }
return "UNION (\n" . implode("\n) UNION (\n", $unions) . "\n)"; return "UNION (\n" . implode("\n) UNION (\n", $unions) . "\n)";
@ -864,7 +677,7 @@ class QueryBuilder extends \yii\base\Object
* @param string|array $columns the columns to be processed * @param string|array $columns the columns to be processed
* @return string the processing result * @return string the processing result
*/ */
protected function buildColumns($columns) public function buildColumns($columns)
{ {
if (!is_array($columns)) { if (!is_array($columns)) {
if (strpos($columns, '(') !== false) { if (strpos($columns, '(') !== false) {
@ -882,4 +695,218 @@ class QueryBuilder extends \yii\base\Object
} }
return is_array($columns) ? implode(', ', $columns) : $columns; return is_array($columns) ? implode(', ', $columns) : $columns;
} }
/**
* Parses the condition specification and generates the corresponding SQL expression.
* @param string|array $condition the condition specification. Please refer to [[Query::where()]]
* on how to specify a condition.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws \yii\db\Exception if the condition is in bad format
*/
public function buildCondition($condition, &$params)
{
static $builders = array(
'AND' => 'buildAndCondition',
'OR' => 'buildAndCondition',
'BETWEEN' => 'buildBetweenCondition',
'NOT BETWEEN' => 'buildBetweenCondition',
'IN' => 'buildInCondition',
'NOT IN' => 'buildInCondition',
'LIKE' => 'buildLikeCondition',
'NOT LIKE' => 'buildLikeCondition',
'OR LIKE' => 'buildLikeCondition',
'OR NOT LIKE' => 'buildLikeCondition',
);
if (!is_array($condition)) {
return (string)$condition;
} elseif ($condition === array()) {
return '';
}
if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
$operator = strtoupper($condition[0]);
if (isset($builders[$operator])) {
$method = $builders[$operator];
array_shift($condition);
return $this->$method($operator, $condition, $params);
} else {
throw new Exception('Found unknown operator in query: ' . $operator);
}
} else { // hash format: 'column1'=>'value1', 'column2'=>'value2', ...
return $this->buildHashCondition($condition, $params);
}
}
private function buildHashCondition($condition, &$params)
{
$parts = array();
foreach ($condition as $column => $value) {
if (is_array($value)) { // IN condition
$parts[] = $this->buildInCondition('in', array($column, $value), $params);
} else {
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if ($value === null) {
$parts[] = "$column IS NULL";
} elseif ($value instanceof Expression) {
$parts[] = "$column=" . $value->expression;
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
} else {
$phName = self::PARAM_PREFIX . count($params);
$parts[] = "$column=$phName";
$params[$phName] = $value;
}
}
}
return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
}
private function buildAndCondition($operator, $operands, &$params)
{
$parts = array();
foreach ($operands as $operand) {
if (is_array($operand)) {
$operand = $this->buildCondition($operand, $params);
}
if ($operand !== '') {
$parts[] = $operand;
}
}
if ($parts !== array()) {
return '(' . implode(") $operator (", $parts) . ')';
} else {
return '';
}
}
private function buildBetweenCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1], $operands[2])) {
throw new Exception("Operator '$operator' requires three operands.");
}
list($column, $value1, $value2) = $operands;
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$phName1 = self::PARAM_PREFIX . count($params);
$phName2 = self::PARAM_PREFIX . count($params);
$params[$phName1] = $value1;
$params[$phName2] = $value2;
return "$column $operator $phName1 AND $phName2";
}
private function buildInCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
$values = (array)$values;
if ($values === array() || $column === array()) {
return $operator === 'IN' ? '0=1' : '';
}
if (count($column) > 1) {
return $this->buildCompositeInCondition($operator, $column, $values, $params);
} elseif (is_array($column)) {
$column = reset($column);
}
foreach ($values as $i => $value) {
if (is_array($value)) {
$value = isset($value[$column]) ? $value[$column] : null;
}
if ($value === null) {
$values[$i] = 'NULL';
} elseif ($value instanceof Expression) {
$values[$i] = $value->expression;
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value;
$values[$i] = $phName;
}
}
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if (count($values) > 1) {
return "$column $operator (" . implode(', ', $values) . ')';
} else {
$operator = $operator === 'IN' ? '=' : '<>';
return "$column$operator{$values[0]}";
}
}
protected function buildCompositeInCondition($operator, $columns, $values, &$params)
{
foreach ($columns as $i => $column) {
if (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
}
}
$vss = array();
foreach ($values as $value) {
$vs = array();
foreach ($columns as $column) {
if (isset($value[$column])) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value[$column];
$vs[] = $phName;
} else {
$vs[] = 'NULL';
}
}
$vss[] = '(' . implode(', ', $vs) . ')';
}
return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')';
}
private function buildLikeCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
$values = (array)$values;
if ($values === array()) {
return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : '';
}
if ($operator === 'LIKE' || $operator === 'NOT LIKE') {
$andor = ' AND ';
} else {
$andor = ' OR ';
$operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE';
}
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$parts = array();
foreach ($values as $value) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value;
$parts[] = "$column $operator $phName";
}
return implode($andor, $parts);
}
} }

16
framework/db/Schema.php

@ -83,7 +83,7 @@ abstract class Schema extends \yii\base\Object
} }
$db = $this->db; $db = $this->db;
$realName = $this->getRealTableName($name); $realName = $this->getRawTableName($name);
if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) {
/** @var $cache Cache */ /** @var $cache Cache */
@ -248,7 +248,7 @@ abstract class Schema extends \yii\base\Object
/** /**
* Quotes a table name for use in a query. * Quotes a table name for use in a query.
* If the table name contains schema prefix, the prefix will also be properly quoted. * If the table name contains schema prefix, the prefix will also be properly quoted.
* If the table name is already quoted or contains special characters including '(', '[[' and '{{', * If the table name is already quoted or contains '(' or '{{',
* then this method will do nothing. * then this method will do nothing.
* @param string $name table name * @param string $name table name
* @return string the properly quoted table name * @return string the properly quoted table name
@ -256,7 +256,7 @@ abstract class Schema extends \yii\base\Object
*/ */
public function quoteTableName($name) public function quoteTableName($name)
{ {
if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { if (strpos($name, '(') !== false || strpos($name, '{{') !== false) {
return $name; return $name;
} }
if (strpos($name, '.') === false) { if (strpos($name, '.') === false) {
@ -273,7 +273,7 @@ abstract class Schema extends \yii\base\Object
/** /**
* Quotes a column name for use in a query. * Quotes a column name for use in a query.
* If the column name contains prefix, the prefix will also be properly quoted. * If the column name contains prefix, the prefix will also be properly quoted.
* If the column name is already quoted or contains special characters including '(', '[[' and '{{', * If the column name is already quoted or contains '(', '[[' or '{{',
* then this method will do nothing. * then this method will do nothing.
* @param string $name column name * @param string $name column name
* @return string the properly quoted column name * @return string the properly quoted column name
@ -318,15 +318,15 @@ abstract class Schema extends \yii\base\Object
} }
/** /**
* Returns the real name of a table name. * Returns the actual name of a given table name.
* This method will strip off curly brackets from the given table name * This method will strip off curly brackets from the given table name
* and replace the percentage character in the name with [[Connection::tablePrefix]]. * and replace the percentage character '%' with [[Connection::tablePrefix]].
* @param string $name the table name to be converted * @param string $name the table name to be converted
* @return string the real name of the given table name * @return string the real name of the given table name
*/ */
public function getRealTableName($name) public function getRawTableName($name)
{ {
if ($this->db->enableAutoQuoting && strpos($name, '{{') !== false) { if (strpos($name, '{{') !== false) {
$name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
return str_replace('%', $this->db->tablePrefix, $name); return str_replace('%', $this->db->tablePrefix, $name);
} else { } else {

23
framework/db/StaleObjectException.php

@ -0,0 +1,23 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class StaleObjectException extends Exception
{
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return \Yii::t('yii|Stale Object Exception');
}
}

6
framework/db/Transaction.php

@ -66,7 +66,7 @@ class Transaction extends \yii\base\Object
if ($this->db === null) { if ($this->db === null) {
throw new InvalidConfigException('Transaction::db must be set.'); throw new InvalidConfigException('Transaction::db must be set.');
} }
\Yii::trace('Starting transaction', __CLASS__); \Yii::trace('Starting transaction', __METHOD__);
$this->db->open(); $this->db->open();
$this->db->pdo->beginTransaction(); $this->db->pdo->beginTransaction();
$this->_active = true; $this->_active = true;
@ -80,7 +80,7 @@ class Transaction extends \yii\base\Object
public function commit() public function commit()
{ {
if ($this->_active && $this->db && $this->db->isActive) { if ($this->_active && $this->db && $this->db->isActive) {
\Yii::trace('Committing transaction', __CLASS__); \Yii::trace('Committing transaction', __METHOD__);
$this->db->pdo->commit(); $this->db->pdo->commit();
$this->_active = false; $this->_active = false;
} else { } else {
@ -95,7 +95,7 @@ class Transaction extends \yii\base\Object
public function rollback() public function rollback()
{ {
if ($this->_active && $this->db && $this->db->isActive) { if ($this->_active && $this->db && $this->db->isActive) {
\Yii::trace('Rolling back transaction', __CLASS__); \Yii::trace('Rolling back transaction', __METHOD__);
$this->db->pdo->rollBack(); $this->db->pdo->rollBack();
$this->_active = false; $this->_active = false;
} else { } else {

323
framework/helpers/ArrayHelper.php

@ -7,9 +7,6 @@
namespace yii\helpers; namespace yii\helpers;
use Yii;
use yii\base\InvalidParamException;
/** /**
* ArrayHelper provides additional array functionality you can use in your * ArrayHelper provides additional array functionality you can use in your
* application. * application.
@ -17,324 +14,6 @@ use yii\base\InvalidParamException;
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class ArrayHelper class ArrayHelper extends base\ArrayHelper
{
/**
* Merges two or more arrays into one recursively.
* If each array has an element with the same string key value, the latter
* will overwrite the former (different from array_merge_recursive).
* Recursive merging will be conducted if both arrays have an element of array
* type and are having the same key.
* For integer-keyed elements, the elements from the latter array will
* be appended to the former array.
* @param array $a array to be merged to
* @param array $b array to be merged from. You can specify additional
* arrays via third argument, fourth argument etc.
* @return array the merged array (the original arrays are not changed.)
*/
public static function merge($a, $b)
{
$args = func_get_args();
$res = array_shift($args);
while ($args !== array()) {
$next = array_shift($args);
foreach ($next as $k => $v) {
if (is_integer($k)) {
isset($res[$k]) ? $res[] = $v : $res[$k] = $v;
} elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
$res[$k] = self::merge($res[$k], $v);
} else {
$res[$k] = $v;
}
}
}
return $res;
}
/**
* Retrieves the value of an array element or object property with the given key or property name.
* If the key does not exist in the array, the default value will be returned instead.
*
* Below are some usage examples,
*
* ~~~
* // working with array
* $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username');
* // working with object
* $username = \yii\helpers\ArrayHelper::getValue($user, 'username');
* // working with anonymous function
* $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) {
* return $user->firstName . ' ' . $user->lastName;
* });
* ~~~
*
* @param array|object $array array or object to extract value from
* @param string|\Closure $key key name of the array element, or property name of the object,
* or an anonymous function returning the value. The anonymous function signature should be:
* `function($array, $defaultValue)`.
* @param mixed $default the default value to be returned if the specified key does not exist
* @return mixed the value of the
*/
public static function getValue($array, $key, $default = null)
{
if ($key instanceof \Closure) {
return $key($array, $default);
} elseif (is_array($array)) {
return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default;
} else {
return $array->$key;
}
}
/**
* Indexes an array according to a specified key.
* The input array should be multidimensional or an array of objects.
*
* The key can be a key name of the sub-array, a property name of object, or an anonymous
* function which returns the key value given an array element.
*
* If a key value is null, the corresponding array element will be discarded and not put in the result.
*
* For example,
*
* ~~~
* $array = array(
* array('id' => '123', 'data' => 'abc'),
* array('id' => '345', 'data' => 'def'),
* );
* $result = ArrayHelper::index($array, 'id');
* // the result is:
* // array(
* // '123' => array('id' => '123', 'data' => 'abc'),
* // '345' => array('id' => '345', 'data' => 'def'),
* // )
*
* // using anonymous function
* $result = ArrayHelper::index($array, function(element) {
* return $element['id'];
* });
* ~~~
*
* @param array $array the array that needs to be indexed
* @param string|\Closure $key the column name or anonymous function whose result will be used to index the array
* @return array the indexed array
*/
public static function index($array, $key)
{
$result = array();
foreach ($array as $element) {
$value = static::getValue($element, $key);
$result[$value] = $element;
}
return $result;
}
/**
* Returns the values of a specified column in an array.
* The input array should be multidimensional or an array of objects.
*
* For example,
*
* ~~~
* $array = array(
* array('id' => '123', 'data' => 'abc'),
* array('id' => '345', 'data' => 'def'),
* );
* $result = ArrayHelper::getColumn($array, 'id');
* // the result is: array( '123', '345')
*
* // using anonymous function
* $result = ArrayHelper::getColumn($array, function(element) {
* return $element['id'];
* });
* ~~~
*
* @param array $array
* @param string|\Closure $name
* @param boolean $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
*/
public static function getColumn($array, $name, $keepKeys = true)
{
$result = array();
if ($keepKeys) {
foreach ($array as $k => $element) {
$result[$k] = static::getValue($element, $name);
}
} else {
foreach ($array as $element) {
$result[] = static::getValue($element, $name);
}
}
return $result;
}
/**
* Builds a map (key-value pairs) from a multidimensional array or an array of objects.
* The `$from` and `$to` parameters specify the key names or property names to set up the map.
* Optionally, one can further group the map according to a grouping field `$group`.
*
* For example,
*
* ~~~
* $array = array(
* array('id' => '123', 'name' => 'aaa', 'class' => 'x'),
* array('id' => '124', 'name' => 'bbb', 'class' => 'x'),
* array('id' => '345', 'name' => 'ccc', 'class' => 'y'),
* );
*
* $result = ArrayHelper::map($array, 'id', 'name');
* // the result is:
* // array(
* // '123' => 'aaa',
* // '124' => 'bbb',
* // '345' => 'ccc',
* // )
*
* $result = ArrayHelper::map($array, 'id', 'name', 'class');
* // the result is:
* // array(
* // 'x' => array(
* // '123' => 'aaa',
* // '124' => 'bbb',
* // ),
* // 'y' => array(
* // '345' => 'ccc',
* // ),
* // )
* ~~~
*
* @param array $array
* @param string|\Closure $from
* @param string|\Closure $to
* @param string|\Closure $group
* @return array
*/
public static function map($array, $from, $to, $group = null)
{ {
$result = array();
foreach ($array as $element) {
$key = static::getValue($element, $from);
$value = static::getValue($element, $to);
if ($group !== null) {
$result[static::getValue($element, $group)][$key] = $value;
} else {
$result[$key] = $value;
}
}
return $result;
}
/**
* Sorts an array of objects or arrays (with the same structure) by one or several keys.
* @param array $array the array to be sorted. The array will be modified after calling this method.
* @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array
* elements, a property name of the objects, or an anonymous function returning the values for comparison
* purpose. The anonymous function signature should be: `function($item)`.
* To sort by multiple keys, provide an array of keys here.
* @param boolean|array $ascending whether to sort in ascending or descending order. When
* sorting by multiple keys with different ascending orders, use an array of ascending flags.
* @param integer|array $sortFlag the PHP sort flag. Valid values include:
* `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last
* value is for sorting strings in case-insensitive manner. Please refer to
* See [PHP manual](http://php.net/manual/en/function.sort.php) for more details.
* When sorting by multiple keys with different sort flags, use an array of sort flags.
* @throws InvalidParamException if the $ascending or $sortFlag parameters do not have
* correct number of elements as that of $key.
*/
public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR)
{
$keys = is_array($key) ? $key : array($key);
if (empty($keys) || empty($array)) {
return;
}
$n = count($keys);
if (is_scalar($ascending)) {
$ascending = array_fill(0, $n, $ascending);
} elseif (count($ascending) !== $n) {
throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.');
}
if (is_scalar($sortFlag)) {
$sortFlag = array_fill(0, $n, $sortFlag);
} elseif (count($sortFlag) !== $n) {
throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.');
}
$args = array();
foreach ($keys as $i => $key) {
$flag = $sortFlag[$i];
if ($flag == (SORT_STRING | SORT_FLAG_CASE)) {
$flag = SORT_STRING;
$column = array();
foreach (static::getColumn($array, $key) as $k => $value) {
$column[$k] = strtolower($value);
}
$args[] = $column;
} else {
$args[] = static::getColumn($array, $key);
}
$args[] = $ascending[$i] ? SORT_ASC : SORT_DESC;
$args[] = $flag;
}
$args[] = &$array;
call_user_func_array('array_multisort', $args);
}
/**
* Encodes special characters in an array of strings into HTML entities.
* Both the array keys and values will be encoded.
* If a value is an array, this method will also encode it recursively.
* @param array $data data to be encoded
* @param boolean $valuesOnly whether to encode array values only. If false,
* both the array keys and array values will be encoded.
* @param string $charset the charset that the data is using. If not set,
* [[\yii\base\Application::charset]] will be used.
* @return array the encoded data
* @see http://www.php.net/manual/en/function.htmlspecialchars.php
*/
public static function htmlEncode($data, $valuesOnly = true, $charset = null)
{
if ($charset === null) {
$charset = Yii::$app->charset;
}
$d = array();
foreach ($data as $key => $value) {
if (!$valuesOnly && is_string($key)) {
$key = htmlspecialchars($key, ENT_QUOTES, $charset);
}
if (is_string($value)) {
$d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset);
} elseif (is_array($value)) {
$d[$key] = static::htmlEncode($value, $charset);
}
}
return $d;
}
/**
* Decodes HTML entities into the corresponding characters in an array of strings.
* Both the array keys and values will be decoded.
* If a value is an array, this method will also decode it recursively.
* @param array $data data to be decoded
* @param boolean $valuesOnly whether to decode array values only. If false,
* both the array keys and array values will be decoded.
* @return array the decoded data
* @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php
*/
public static function htmlDecode($data, $valuesOnly = true)
{
$d = array();
foreach ($data as $key => $value) {
if (!$valuesOnly && is_string($key)) {
$key = htmlspecialchars_decode($key, ENT_QUOTES);
}
if (is_string($value)) {
$d[$key] = htmlspecialchars_decode($value, ENT_QUOTES);
} elseif (is_array($value)) {
$d[$key] = static::htmlDecode($value);
}
}
return $d;
}
} }

449
framework/helpers/ConsoleColor.php

@ -18,453 +18,6 @@ namespace yii\helpers;
* @author Carsten Brandt <mail@cebe.cc> * @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
class ConsoleColor class ConsoleColor extends base\ConsoleColor
{ {
const FG_BLACK = 30;
const FG_RED = 31;
const FG_GREEN = 32;
const FG_YELLOW = 33;
const FG_BLUE = 34;
const FG_PURPLE = 35;
const FG_CYAN = 36;
const FG_GREY = 37;
const BG_BLACK = 40;
const BG_RED = 41;
const BG_GREEN = 42;
const BG_YELLOW = 43;
const BG_BLUE = 44;
const BG_PURPLE = 45;
const BG_CYAN = 46;
const BG_GREY = 47;
const BOLD = 1;
const ITALIC = 3;
const UNDERLINE = 4;
const BLINK = 5;
const NEGATIVE = 7;
const CONCEALED = 8;
const CROSSED_OUT = 9;
const FRAMED = 51;
const ENCIRCLED = 52;
const OVERLINED = 53;
/**
* Moves the terminal cursor up by sending ANSI control code CUU to the terminal.
* If the cursor is already at the edge of the screen, this has no effect.
* @param integer $rows number of rows the cursor should be moved up
*/
public static function moveCursorUp($rows=1)
{
echo "\033[" . (int) $rows . 'A';
}
/**
* Moves the terminal cursor down by sending ANSI control code CUD to the terminal.
* If the cursor is already at the edge of the screen, this has no effect.
* @param integer $rows number of rows the cursor should be moved down
*/
public static function moveCursorDown($rows=1)
{
echo "\033[" . (int) $rows . 'B';
}
/**
* Moves the terminal cursor forward by sending ANSI control code CUF to the terminal.
* If the cursor is already at the edge of the screen, this has no effect.
* @param integer $steps number of steps the cursor should be moved forward
*/
public static function moveCursorForward($steps=1)
{
echo "\033[" . (int) $steps . 'C';
}
/**
* Moves the terminal cursor backward by sending ANSI control code CUB to the terminal.
* If the cursor is already at the edge of the screen, this has no effect.
* @param integer $steps number of steps the cursor should be moved backward
*/
public static function moveCursorBackward($steps=1)
{
echo "\033[" . (int) $steps . 'D';
}
/**
* Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal.
* @param integer $lines number of lines the cursor should be moved down
*/
public static function moveCursorNextLine($lines=1)
{
echo "\033[" . (int) $lines . 'E';
}
/**
* Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal.
* @param integer $lines number of lines the cursor should be moved up
*/
public static function moveCursorPrevLine($lines=1)
{
echo "\033[" . (int) $lines . 'F';
}
/**
* Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal.
* @param integer $column 1-based column number, 1 is the left edge of the screen.
* @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line.
*/
public static function moveCursorTo($column, $row=null)
{
if ($row === null) {
echo "\033[" . (int) $column . 'G';
} else {
echo "\033[" . (int) $row . ';' . (int) $column . 'H';
}
}
/**
* Scrolls whole page up by sending ANSI control code SU to the terminal.
* New lines are added at the bottom. This is not supported by ANSI.SYS used in windows.
* @param int $lines number of lines to scroll up
*/
public static function scrollUp($lines=1)
{
echo "\033[".(int)$lines."S";
}
/**
* Scrolls whole page down by sending ANSI control code SD to the terminal.
* New lines are added at the top. This is not supported by ANSI.SYS used in windows.
* @param int $lines number of lines to scroll down
*/
public static function scrollDown($lines=1)
{
echo "\033[".(int)$lines."T";
}
/**
* Saves the current cursor position by sending ANSI control code SCP to the terminal.
* Position can then be restored with {@link restoreCursorPosition}.
*/
public static function saveCursorPosition()
{
echo "\033[s";
}
/**
* Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal.
*/
public static function restoreCursorPosition()
{
echo "\033[u";
}
/**
* Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal.
* Use {@link showCursor} to bring it back.
* Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit.
*/
public static function hideCursor()
{
echo "\033[?25l";
}
/**
* Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal.
*/
public static function showCursor()
{
echo "\033[?25h";
}
/**
* Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal.
* Cursor position will not be changed.
* **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen.
*/
public static function clearScreen()
{
echo "\033[2J";
}
/**
* Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal.
* Cursor position will not be changed.
*/
public static function clearScreenBeforeCursor()
{
echo "\033[1J";
}
/**
* Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal.
* Cursor position will not be changed.
*/
public static function clearScreenAfterCursor()
{
echo "\033[0J";
}
/**
* Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal.
* Cursor position will not be changed.
*/
public static function clearLine()
{
echo "\033[2K";
}
/**
* Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal.
* Cursor position will not be changed.
*/
public static function clearLineBeforeCursor()
{
echo "\033[1K";
}
/**
* Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal.
* Cursor position will not be changed.
*/
public static function clearLineAfterCursor()
{
echo "\033[0K";
}
/**
* Will send ANSI format for following output
*
* You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg
* TODO: documentation
*/
public static function ansiStyle()
{
echo "\033[" . implode(';', func_get_args()) . 'm';
}
/**
* Will return a string formatted with the given ANSI style
*
* See {@link ansiStyle} for possible arguments.
* @param string $string the string to be formatted
* @return string
*/
public static function ansiStyleString($string)
{
$args = func_get_args();
array_shift($args);
$code = implode(';', $args);
return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m";
}
//const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors
public static function xterm256ColorFg($i) // TODO naming!
{
return '38;5;'.$i;
}
public static function xterm256ColorBg($i) // TODO naming!
{
return '48;5;'.$i;
}
/**
* Usage: list($w, $h) = ConsoleHelper::getScreenSize();
*
* @return array
*/
public static function getScreenSize()
{
// TODO implement
return array(150,50);
}
/**
* resets any ansi style set by previous method {@link ansiStyle}
* Any output after this is will have default text style.
*/
public static function reset()
{
echo "\033[0m";
}
/**
* Strips ANSI control codes from a string
*
* @param string $string String to strip
* @return string
*/
public static function strip($string)
{
return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color
}
// TODO refactor and review
public static function ansiToHtml($string)
{
$tags = 0;
return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) {
$styleA = array();
foreach(explode(';', $ansi) as $controlCode)
{
switch($controlCode)
{
case static::FG_BLACK: $style = array('color' => '#000000'); break;
case static::FG_BLUE: $style = array('color' => '#000078'); break;
case static::FG_CYAN: $style = array('color' => '#007878'); break;
case static::FG_GREEN: $style = array('color' => '#007800'); break;
case static::FG_GREY: $style = array('color' => '#787878'); break;
case static::FG_PURPLE: $style = array('color' => '#780078'); break;
case static::FG_RED: $style = array('color' => '#780000'); break;
case static::FG_YELLOW: $style = array('color' => '#787800'); break;
case static::BG_BLACK: $style = array('background-color' => '#000000'); break;
case static::BG_BLUE: $style = array('background-color' => '#000078'); break;
case static::BG_CYAN: $style = array('background-color' => '#007878'); break;
case static::BG_GREEN: $style = array('background-color' => '#007800'); break;
case static::BG_GREY: $style = array('background-color' => '#787878'); break;
case static::BG_PURPLE: $style = array('background-color' => '#780078'); break;
case static::BG_RED: $style = array('background-color' => '#780000'); break;
case static::BG_YELLOW: $style = array('background-color' => '#787800'); break;
case static::BOLD: $style = array('font-weight' => 'bold'); break;
case static::ITALIC: $style = array('font-style' => 'italic'); break;
case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break;
case static::OVERLINED: $style = array('text-decoration' => array('overline')); break;
case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break;
case static::BLINK: $style = array('text-decoration' => array('blink')); break;
case static::NEGATIVE: // ???
case static::CONCEALED:
case static::ENCIRCLED:
case static::FRAMED:
// TODO allow resetting codes
break;
case 0: // ansi reset
$return = '';
for($n=$tags; $tags>0; $tags--) {
$return .= '</span>';
}
return $return;
}
$styleA = ArrayHelper::merge($styleA, $style);
}
$styleString[] = array();
foreach($styleA as $name => $content) {
if ($name === 'text-decoration') {
$content = implode(' ', $content);
}
$styleString[] = $name.':'.$content;
}
$tags++;
return '<span' . (!empty($styleString) ? 'style="' . implode(';', $styleString) : '') . '>';
}, $string);
}
/**
* TODO syntax copied from https://github.com/pear/Console_Color2/blob/master/Console/Color2.php
*
* Converts colorcodes in the format %y (for yellow) into ansi-control
* codes. The conversion table is: ('bold' meaning 'light' on some
* terminals). It's almost the same conversion table irssi uses.
* <pre>
* text text background
* ------------------------------------------------
* %k %K %0 black dark grey black
* %r %R %1 red bold red red
* %g %G %2 green bold green green
* %y %Y %3 yellow bold yellow yellow
* %b %B %4 blue bold blue blue
* %m %M %5 magenta bold magenta magenta
* %p %P magenta (think: purple)
* %c %C %6 cyan bold cyan cyan
* %w %W %7 white bold white white
*
* %F Blinking, Flashing
* %U Underline
* %8 Reverse
* %_,%9 Bold
*
* %n Resets the color
* %% A single %
* </pre>
* First param is the string to convert, second is an optional flag if
* colors should be used. It defaults to true, if set to false, the
* colorcodes will just be removed (And %% will be transformed into %)
*
* @param string $string String to convert
* @param bool $colored Should the string be colored?
*
* @return string
*/
public static function renderColoredString($string)
{
$colored = true;
static $conversions = array ( // static so the array doesn't get built
// everytime
// %y - yellow, and so on... {{{
'%y' => array('color' => 'yellow'),
'%g' => array('color' => 'green' ),
'%b' => array('color' => 'blue' ),
'%r' => array('color' => 'red' ),
'%p' => array('color' => 'purple'),
'%m' => array('color' => 'purple'),
'%c' => array('color' => 'cyan' ),
'%w' => array('color' => 'grey' ),
'%k' => array('color' => 'black' ),
'%n' => array('color' => 'reset' ),
'%Y' => array('color' => 'yellow', 'style' => 'light'),
'%G' => array('color' => 'green', 'style' => 'light'),
'%B' => array('color' => 'blue', 'style' => 'light'),
'%R' => array('color' => 'red', 'style' => 'light'),
'%P' => array('color' => 'purple', 'style' => 'light'),
'%M' => array('color' => 'purple', 'style' => 'light'),
'%C' => array('color' => 'cyan', 'style' => 'light'),
'%W' => array('color' => 'grey', 'style' => 'light'),
'%K' => array('color' => 'black', 'style' => 'light'),
'%N' => array('color' => 'reset', 'style' => 'light'),
'%3' => array('background' => 'yellow'),
'%2' => array('background' => 'green' ),
'%4' => array('background' => 'blue' ),
'%1' => array('background' => 'red' ),
'%5' => array('background' => 'purple'),
'%6' => array('background' => 'cyan' ),
'%7' => array('background' => 'grey' ),
'%0' => array('background' => 'black' ),
// Don't use this, I can't stand flashing text
'%F' => array('style' => 'blink'),
'%U' => array('style' => 'underline'),
'%8' => array('style' => 'inverse'),
'%9' => array('style' => 'bold'),
'%_' => array('style' => 'bold')
// }}}
);
if ($colored) {
$string = str_replace('%%', '% ', $string);
foreach ($conversions as $key => $value) {
$string = str_replace($key, Console_Color::color($value),
$string);
}
$string = str_replace('% ', '%', $string);
} else {
$string = preg_replace('/%((%)|.)/', '$2', $string);
}
return $string;
}
/**
* Escapes % so they don't get interpreted as color codes
*
* @param string $string String to escape
*
* @access public
* @return string
*/
public static function escape($string)
{
return str_replace('%', '%%', $string);
}
} }

255
framework/helpers/FileHelper.php

@ -9,9 +9,6 @@
namespace yii\helpers; namespace yii\helpers;
use yii\base\Exception;
use yii\base\InvalidConfigException;
/** /**
* Filesystem helper * Filesystem helper
* *
@ -19,256 +16,6 @@ use yii\base\InvalidConfigException;
* @author Alex Makarov <sam@rmcreative.ru> * @author Alex Makarov <sam@rmcreative.ru>
* @since 2.0 * @since 2.0
*/ */
class FileHelper class FileHelper extends base\FileHelper
{
/**
* Returns the extension name of a file path.
* For example, the path "path/to/something.php" would return "php".
* @param string $path the file path
* @return string the extension name without the dot character.
*/
public static function getExtension($path)
{
return pathinfo($path, PATHINFO_EXTENSION);
}
/**
* Checks the given path and ensures it is a directory.
* This method will call `realpath()` to "normalize" the given path.
* If the given path does not refer to an existing directory, an exception will be thrown.
* @param string $path the given path. This can also be a path alias.
* @return string the normalized path
* @throws InvalidConfigException if the path does not refer to an existing directory.
*/
public static function ensureDirectory($path)
{
$p = \Yii::getAlias($path);
if (($p = realpath($p)) !== false && is_dir($p)) {
return $p;
} else {
throw new InvalidConfigException('Directory does not exist: ' . $path);
}
}
/**
* Normalizes a file/directory path.
* After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`,
* and any trailing directory separators will be removed. For example, '/home\demo/' on Linux
* will be normalized as '/home/demo'.
* @param string $path the file/directory path to be normalized
* @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`.
* @return string the normalized file/directory path
*/
public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR)
{
return rtrim(strtr($path, array('/' => $ds, '\\' => $ds)), $ds);
}
/**
* Returns the localized version of a specified file.
*
* The searching is based on the specified language code. In particular,
* a file with the same name will be looked for under the subdirectory
* whose name is same as the language code. For example, given the file "path/to/view.php"
* and language code "zh_cn", the localized file will be looked for as
* "path/to/zh_cn/view.php". If the file is not found, the original file
* will be returned.
*
* If the target and the source language codes are the same,
* the original file will be returned.
*
* For consistency, it is recommended that the language code is given
* in lower case and in the format of LanguageID_RegionID (e.g. "en_us").
*
* @param string $file the original file
* @param string $language the target language that the file should be localized to.
* If not set, the value of [[\yii\base\Application::language]] will be used.
* @param string $sourceLanguage the language that the original file is in.
* If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used.
* @return string the matching localized file, or the original file if the localized version is not found.
* If the target and the source language codes are the same, the original file will be returned.
*/
public static function localize($file, $language = null, $sourceLanguage = null)
{
if ($language === null) {
$language = \Yii::$app->language;
}
if ($sourceLanguage === null) {
$sourceLanguage = \Yii::$app->sourceLanguage;
}
if ($language === $sourceLanguage) {
return $file;
}
$desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $sourceLanguage . DIRECTORY_SEPARATOR . basename($file);
return is_file($desiredFile) ? $desiredFile : $file;
}
/**
* Determines the MIME type of the specified file.
* This method will first try to determine the MIME type based on
* [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will
* fall back to [[getMimeTypeByExtension()]].
* @param string $file the file name.
* @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`.
* This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php).
* @param boolean $checkExtension whether to use the file extension to determine the MIME type in case
* `finfo_open()` cannot determine it.
* @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined.
*/
public static function getMimeType($file, $magicFile = null, $checkExtension = true)
{
if (function_exists('finfo_open')) {
$info = finfo_open(FILEINFO_MIME_TYPE, $magicFile);
if ($info && ($result = finfo_file($info, $file)) !== false) {
return $result;
}
}
return $checkExtension ? self::getMimeTypeByExtension($file) : null;
}
/**
* Determines the MIME type based on the extension name of the specified file.
* This method will use a local map between extension names and MIME types.
* @param string $file the file name.
* @param string $magicFile the path of the file that contains all available MIME type information.
* If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used.
* @return string the MIME type. Null is returned if the MIME type cannot be determined.
*/
public static function getMimeTypeByExtension($file, $magicFile = null)
{
if ($magicFile === null) {
$magicFile = \Yii::getAlias('@yii/util/mimeTypes.php');
}
$mimeTypes = require($magicFile);
if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') {
$ext = strtolower($ext);
if (isset($mimeTypes[$ext])) {
return $mimeTypes[$ext];
}
}
return null;
}
/**
* Copies a list of files from one place to another.
* @param array $fileList the list of files to be copied (name=>spec).
* The array keys are names displayed during the copy process, and array values are specifications
* for files to be copied. Each array value must be an array of the following structure:
* <ul>
* <li>source: required, the full path of the file/directory to be copied from</li>
* <li>target: required, the full path of the file/directory to be copied to</li>
* <li>callback: optional, the callback to be invoked when copying a file. The callback function
* should be declared as follows:
* <pre>
* function foo($source,$params)
* </pre>
* where $source parameter is the source file path, and the content returned
* by the function will be saved into the target file.</li>
* <li>params: optional, the parameters to be passed to the callback</li>
* </ul>
* @see buildFileList
*/
public static function copyFiles($fileList)
{
$overwriteAll = false;
foreach($fileList as $name=>$file) {
$source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR);
$target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR);
$callback = isset($file['callback']) ? $file['callback'] : null;
$params = isset($file['params']) ? $file['params'] : null;
if(is_dir($source)) {
try {
self::ensureDirectory($target);
}
catch (Exception $e) {
mkdir($target, true, 0777);
}
continue;
}
if($callback !== null) {
$content = call_user_func($callback, $source, $params);
}
else {
$content = file_get_contents($source);
}
if(is_file($target)) {
if($content === file_get_contents($target)) {
echo " unchanged $name\n";
continue;
}
if($overwriteAll) {
echo " overwrite $name\n";
}
else {
echo " exist $name\n";
echo " ...overwrite? [Yes|No|All|Quit] ";
$answer = trim(fgets(STDIN));
if(!strncasecmp($answer, 'q', 1)) {
return;
}
elseif(!strncasecmp($answer, 'y', 1)) {
echo " overwrite $name\n";
}
elseif(!strncasecmp($answer, 'a', 1)) {
echo " overwrite $name\n";
$overwriteAll = true;
}
else {
echo " skip $name\n";
continue;
}
}
}
else {
try {
self::ensureDirectory(dirname($target));
}
catch (Exception $e) {
mkdir(dirname($target), true, 0777);
}
echo " generate $name\n";
}
file_put_contents($target, $content);
}
}
/**
* Builds the file list of a directory.
* This method traverses through the specified directory and builds
* a list of files and subdirectories that the directory contains.
* The result of this function can be passed to {@link copyFiles}.
* @param string $sourceDir the source directory
* @param string $targetDir the target directory
* @param string $baseDir base directory
* @param array $ignoreFiles list of the names of files that should
* be ignored in list building process. Argument available since 1.1.11.
* @param array $renameMap hash array of file names that should be
* renamed. Example value: array('1.old.txt'=>'2.new.txt').
* @return array the file list (see {@link copyFiles})
*/
public static function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array())
{ {
$list = array();
$handle = opendir($sourceDir);
while(($file = readdir($handle)) !== false) {
if(in_array($file, array('.', '..', '.svn', '.gitignore')) || in_array($file, $ignoreFiles)) {
continue;
}
$sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file;
$targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap);
$name = $baseDir === '' ? $file : $baseDir.'/'.$file;
$list[$name] = array(
'source' => $sourcePath,
'target' => $targetPath,
);
if(is_dir($sourcePath)) {
$list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap));
}
}
closedir($handle);
return $list;
}
} }

965
framework/helpers/Html.php

@ -7,975 +7,12 @@
namespace yii\helpers; namespace yii\helpers;
use Yii;
use yii\base\InvalidParamException;
/** /**
* Html provides a set of static methods for generating commonly used HTML tags. * Html provides a set of static methods for generating commonly used HTML tags.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class Html class Html extends base\Html
{
/**
* @var boolean whether to close void (empty) elements. Defaults to true.
* @see voidElements
*/
public static $closeVoidElements = true;
/**
* @var array list of void elements (element name => 1)
* @see closeVoidElements
* @see http://www.w3.org/TR/html-markup/syntax.html#void-element
*/
public static $voidElements = array(
'area' => 1,
'base' => 1,
'br' => 1,
'col' => 1,
'command' => 1,
'embed' => 1,
'hr' => 1,
'img' => 1,
'input' => 1,
'keygen' => 1,
'link' => 1,
'meta' => 1,
'param' => 1,
'source' => 1,
'track' => 1,
'wbr' => 1,
);
/**
* @var boolean whether to show the values of boolean attributes in element tags.
* If false, only the attribute names will be generated.
* @see booleanAttributes
*/
public static $showBooleanAttributeValues = true;
/**
* @var array list of boolean attributes. The presence of a boolean attribute on
* an element represents the true value, and the absence of the attribute represents the false value.
* @see showBooleanAttributeValues
* @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes
*/
public static $booleanAttributes = array(
'async' => 1,
'autofocus' => 1,
'autoplay' => 1,
'checked' => 1,
'controls' => 1,
'declare' => 1,
'default' => 1,
'defer' => 1,
'disabled' => 1,
'formnovalidate' => 1,
'hidden' => 1,
'ismap' => 1,
'loop' => 1,
'multiple' => 1,
'muted' => 1,
'nohref' => 1,
'noresize' => 1,
'novalidate' => 1,
'open' => 1,
'readonly' => 1,
'required' => 1,
'reversed' => 1,
'scoped' => 1,
'seamless' => 1,
'selected' => 1,
'typemustmatch' => 1,
);
/**
* @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes
* that are rendered by [[renderAttributes()]].
*/
public static $attributeOrder = array(
'type',
'id',
'class',
'name',
'value',
'href',
'src',
'action',
'method',
'selected',
'checked',
'readonly',
'disabled',
'multiple',
'size',
'maxlength',
'width',
'height',
'rows',
'cols',
'alt',
'title',
'rel',
'media',
);
/**
* Encodes special characters into HTML entities.
* The [[yii\base\Application::charset|application charset]] will be used for encoding.
* @param string $content the content to be encoded
* @return string the encoded content
* @see decode
* @see http://www.php.net/manual/en/function.htmlspecialchars.php
*/
public static function encode($content)
{
return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset);
}
/**
* Decodes special HTML entities back to the corresponding characters.
* This is the opposite of [[encode()]].
* @param string $content the content to be decoded
* @return string the decoded content
* @see encode
* @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php
*/
public static function decode($content)
{
return htmlspecialchars_decode($content, ENT_QUOTES);
}
/**
* Generates a complete HTML tag.
* @param string $name the tag name
* @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded.
* If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks.
* @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.
* @return string the generated HTML tag
* @see beginTag
* @see endTag
*/
public static function tag($name, $content = '', $options = array())
{
$html = '<' . $name . static::renderTagAttributes($options);
if (isset(static::$voidElements[strtolower($name)])) {
return $html . (static::$closeVoidElements ? ' />' : '>');
} else {
return $html . ">$content</$name>";
}
}
/**
* Generates a start tag.
* @param string $name the tag name
* @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.
* @return string the generated start tag
* @see endTag
* @see tag
*/
public static function beginTag($name, $options = array())
{
return '<' . $name . static::renderTagAttributes($options) . '>';
}
/**
* Generates an end tag.
* @param string $name the tag name
* @return string the generated end tag
* @see beginTag
* @see tag
*/
public static function endTag($name)
{
return "</$name>";
}
/**
* Encloses the given content within a CDATA tag.
* @param string $content the content to be enclosed within the CDATA tag
* @return string the CDATA tag with the enclosed content.
*/
public static function cdata($content)
{
return '<![CDATA[' . $content . ']]>';
}
/**
* Generates a style tag.
* @param string $content the style content
* @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.
* If the options does not contain "type", a "type" attribute with value "text/css" will be used.
* @return string the generated style tag
*/
public static function style($content, $options = array())
{
if (!isset($options['type'])) {
$options['type'] = 'text/css';
}
return static::tag('style', "/*<![CDATA[*/\n{$content}\n/*]]>*/", $options);
}
/**
* Generates a script tag.
* @param string $content the script content
* @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.
* If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered.
* @return string the generated script tag
*/
public static function script($content, $options = array())
{
if (!isset($options['type'])) {
$options['type'] = 'text/javascript';
}
return static::tag('script', "/*<![CDATA[*/\n{$content}\n/*]]>*/", $options);
}
/**
* Generates a link tag that refers to an external CSS file.
* @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]].
* @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.
* @return string the generated link tag
* @see url
*/
public static function cssFile($url, $options = array())
{
$options['rel'] = 'stylesheet';
$options['type'] = 'text/css';
$options['href'] = static::url($url);
return static::tag('link', '', $options);
}
/**
* Generates a script tag that refers to an external JavaScript file.
* @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]].
* @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.
* @return string the generated script tag
* @see url
*/
public static function jsFile($url, $options = array())
{
$options['type'] = 'text/javascript';
$options['src'] = static::url($url);
return static::tag('script', '', $options);
}
/**
* Generates a form start tag.
* @param array|string $action the form action URL. This parameter will be processed by [[url()]].
* @param string $method the form submission method, either "post" or "get" (case-insensitive)
* @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.
* @return string the generated form start tag.
* @see endForm
*/
public static function beginForm($action = '', $method = 'post', $options = array())
{
$action = static::url($action);
// query parameters in the action are ignored for GET method
// we use hidden fields to add them back
$hiddens = array();
if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) {
foreach (explode('&', substr($action, $pos + 1)) as $pair) {
if (($pos1 = strpos($pair, '=')) !== false) {
$hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1)));
} else {
$hiddens[] = static::hiddenInput(urldecode($pair), '');
}
}
$action = substr($action, 0, $pos);
}
$options['action'] = $action;
$options['method'] = $method;
$form = static::beginTag('form', $options);
if ($hiddens !== array()) {
$form .= "\n" . implode("\n", $hiddens);
}
return $form;
}
/**
* Generates a form end tag.
* @return string the generated tag
* @see beginForm
*/
public static function endForm()
{
return '</form>';
}
/**
* Generates a hyperlink tag.
* @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 is coming from end users, you should consider [[encode()]]
* it to prevent XSS attacks.
* @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]]
* and will be used for the "href" attribute of the tag. If this parameter is null, the "href" 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.
* @return string the generated hyperlink
* @see url
*/
public static function a($text, $url = null, $options = array())
{
if ($url !== null) {
$options['href'] = static::url($url);
}
return static::tag('a', $text, $options);
}
/**
* Generates a mailto hyperlink.
* @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 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
* 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()]].
* If a value is null, the corresponding attribute will not be rendered.
* @return string the generated mailto link
*/
public static function mailto($text, $email = null, $options = array())
{
return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options);
}
/**
* Generates an image tag.
* @param string $src the image URL. This parameter will be processed by [[url()]].
* @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.
* @return string the generated image tag
*/
public static function img($src, $options = array())
{
$options['src'] = static::url($src);
if (!isset($options['alt'])) {
$options['alt'] = '';
}
return static::tag('img', null, $options);
}
/**
* Generates a label tag.
* @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 consider [[encode()]]
* it to prevent XSS attacks.
* @param string $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()]].
* If a value is null, the corresponding attribute will not be rendered.
* @return string the generated label tag
*/
public static function label($content, $for = null, $options = array())
{
$options['for'] = $for;
return static::tag('label', $content, $options);
}
/**
* Generates a button tag.
* @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 $content the content enclosed within the button tag. 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 consider [[encode()]] it to prevent XSS attacks.
* @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.
* If the options does not contain "type", a "type" attribute with value "button" will be rendered.
* @return string the generated button tag
*/
public static function button($name = null, $value = null, $content = 'Button', $options = array())
{
$options['name'] = $name;
$options['value'] = $value;
if (!isset($options['type'])) {
$options['type'] = 'button';
}
return static::tag('button', $content, $options);
}
/**
* Generates a submit button tag.
* @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 $content the content enclosed within the button tag. 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 consider [[encode()]] it to prevent XSS attacks.
* @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.
* @return string the generated submit button tag
*/
public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array())
{
$options['type'] = 'submit';
return static::button($name, $value, $content, $options);
}
/**
* Generates a reset button tag.
* @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 $content the content enclosed within the button tag. 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 consider [[encode()]] it to prevent XSS attacks.
* @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.
* @return string the generated reset button tag
*/
public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array())
{
$options['type'] = 'reset';
return static::button($name, $value, $content, $options);
}
/**
* 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 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.
* @return string the generated input tag
*/
public static function input($type, $name = null, $value = null, $options = array())
{
$options['type'] = $type;
$options['name'] = $name;
$options['value'] = $value;
return static::tag('input', null, $options);
}
/**
* Generates an input button.
* @param string $name the name attribute.
* @param string $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.
* @return string the generated button tag
*/
public static function buttonInput($name, $value = 'Button', $options = array())
{
return static::input('button', $name, $value, $options);
}
/**
* Generates a submit input button.
* @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 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.
* @return string the generated button tag
*/
public static function submitInput($name = null, $value = 'Submit', $options = array())
{
return static::input('submit', $name, $value, $options);
}
/**
* Generates a reset input button.
* @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 array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]].
* Attributes whose value is null will be ignored and not put in the tag returned.
* @return string the generated button tag
*/
public static function resetInput($name = null, $value = 'Reset', $options = array())
{
return static::input('reset', $name, $value, $options);
}
/**
* 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 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.
* @return string the generated button tag
*/
public static function textInput($name, $value = null, $options = array())
{
return static::input('text', $name, $value, $options);
}
/**
* 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 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.
* @return string the generated button tag
*/
public static function hiddenInput($name, $value = null, $options = array())
{
return static::input('hidden', $name, $value, $options);
}
/**
* 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 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.
* @return string the generated button tag
*/
public static function passwordInput($name, $value = null, $options = array())
{
return static::input('password', $name, $value, $options);
}
/**
* Generates a file input field.
* To use a file input field, you should set the enclosing form's "enctype" attribute to
* 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 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.
* @return string the generated button tag
*/
public static function fileInput($name, $value = null, $options = array())
{
return static::input('file', $name, $value, $options);
}
/**
* Generates a text area input.
* @param string $name the input name
* @param string $value the input value. Note that it will be encoded using [[encode()]].
* @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.
* @return string the generated text area tag
*/
public static function textarea($name, $value = '', $options = array())
{
$options['name'] = $name;
return static::tag('textarea', static::encode($value), $options);
}
/**
* Generates a radio button input.
* @param string $name the name attribute.
* @param boolean $checked whether the radio button should be checked.
* @param string $value the value attribute. If it is null, the value attribute will not be rendered.
* @param array $options the tag options in terms of name-value pairs. The following options are supported:
*
* - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute
* is present, a hidden input will be generated so that if the radio button is not checked and is submitted,
* the value of this attribute will still be submitted to the server via the hidden input.
*
* 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.
*
* @return string the generated radio button tag
*/
public static function radio($name, $checked = false, $value = '1', $options = array())
{
$options['checked'] = $checked;
$options['value'] = $value;
if (isset($options['uncheck'])) {
// add a hidden field so that if the radio button is not selected, it still submits a value
$hidden = static::hiddenInput($name, $options['uncheck']);
unset($options['uncheck']);
} else {
$hidden = '';
}
return $hidden . static::input('radio', $name, $value, $options);
}
/**
* Generates a checkbox input.
* @param string $name the name attribute.
* @param boolean $checked whether the checkbox should be checked.
* @param string $value the value attribute. If it is null, the value attribute will not be rendered.
* @param array $options the tag options in terms of name-value pairs. The following options are supported:
*
* - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute
* is present, a hidden input will be generated so that if the checkbox is not checked and is submitted,
* the value of this attribute will still be submitted to the server via the hidden input.
*
* 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.
*
* @return string the generated checkbox tag
*/
public static function checkbox($name, $checked = false, $value = '1', $options = array())
{
$options['checked'] = $checked;
$options['value'] = $value;
if (isset($options['uncheck'])) {
// add a hidden field so that if the checkbox is not selected, it still submits a value
$hidden = static::hiddenInput($name, $options['uncheck']);
unset($options['uncheck']);
} else {
$hidden = '';
}
return $hidden . static::input('checkbox', $name, $value, $options);
}
/**
* Generates a drop-down list.
* @param string $name the input name
* @param string $selection the selected value
* @param array $items the option data items. The array keys are option values, and the array values
* are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
* For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
* If you have a list of data models, you may convert them into the format described above using
* [[\yii\helpers\ArrayHelper::map()]].
*
* Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in
* the labels will also be HTML-encoded.
* @param array $options the tag options in terms of name-value pairs. The following options are supported:
*
* - prompt: string, a prompt text to be displayed as the first option;
* - options: array, the attributes for the select option tags. The array keys must be valid option values,
* and the array values are the extra attributes for the corresponding option tags. For example,
*
* ~~~
* array(
* 'value1' => array('disabled' => true),
* 'value2' => array('label' => 'value 2'),
* );
* ~~~
*
* - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options',
* except that the array keys represent the optgroup labels specified in $items.
*
* 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.
*
* @return string the generated drop-down list tag
*/
public static function dropDownList($name, $selection = null, $items = array(), $options = array())
{
$options['name'] = $name;
$selectOptions = static::renderSelectOptions($selection, $items, $options);
return static::tag('select', "\n" . $selectOptions . "\n", $options);
}
/**
* Generates a list box.
* @param string $name the input name
* @param string|array $selection the selected value(s)
* @param array $items the option data items. The array keys are option values, and the array values
* are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
* For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
* If you have a list of data models, you may convert them into the format described above using
* [[\yii\helpers\ArrayHelper::map()]].
*
* Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in
* the labels will also be HTML-encoded.
* @param array $options the tag options in terms of name-value pairs. The following options are supported:
*
* - prompt: string, a prompt text to be displayed as the first option;
* - options: array, the attributes for the select option tags. The array keys must be valid option values,
* and the array values are the extra attributes for the corresponding option tags. For example,
*
* ~~~
* array(
* 'value1' => array('disabled' => true),
* 'value2' => array('label' => 'value 2'),
* );
* ~~~
*
* - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options',
* except that the array keys represent the optgroup labels specified in $items.
* - unselect: string, the value that will be submitted when no option is selected.
* When this attribute is set, a hidden field will be generated so that if no option is selected in multiple
* mode, we can still obtain the posted unselect value.
*
* 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.
*
* @return string the generated list box tag
*/
public static function listBox($name, $selection = null, $items = array(), $options = array())
{ {
if (!isset($options['size'])) {
$options['size'] = 4;
}
if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') {
$name .= '[]';
}
$options['name'] = $name;
if (isset($options['unselect'])) {
// add a hidden field so that if the list box has no option being selected, it still submits a value
if (substr($name, -2) === '[]') {
$name = substr($name, 0, -2);
}
$hidden = static::hiddenInput($name, $options['unselect']);
unset($options['unselect']);
} else {
$hidden = '';
}
$selectOptions = static::renderSelectOptions($selection, $items, $options);
return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options);
}
/**
* Generates a list of checkboxes.
* A checkbox list allows multiple selection, like [[listBox()]].
* As a result, the corresponding submitted value is an array.
* @param string $name the name attribute of each checkbox.
* @param string|array $selection the selected value(s).
* @param array $items the data item used to generate the checkboxes.
* The array keys are the labels, while the array values are the corresponding checkbox values.
* Note that the labels will NOT be HTML-encoded, while the values will.
* @param array $options options (name => config) for the checkbox list. The following options are supported:
*
* - unselect: string, the value that should be submitted when none of the checkboxes is selected.
* By setting this option, a hidden input will be generated.
* - separator: string, the HTML code that separates items.
* - item: callable, a callback that can be used to customize the generation of the HTML code
* corresponding to a single item in $items. The signature of this callback must be:
*
* ~~~
* function ($index, $label, $name, $checked, $value)
* ~~~
*
* where $index is the zero-based index of the checkbox in the whole list; $label
* is the label for the checkbox; and $name, $value and $checked represent the name,
* value and the checked status of the checkbox input.
* @return string the generated checkbox list
*/
public static function checkboxList($name, $selection = null, $items = array(), $options = array())
{
if (substr($name, -2) !== '[]') {
$name .= '[]';
}
$formatter = isset($options['item']) ? $options['item'] : null;
$lines = array();
$index = 0;
foreach ($items as $value => $label) {
$checked = $selection !== null &&
(!is_array($selection) && !strcmp($value, $selection)
|| is_array($selection) && in_array($value, $selection));
if ($formatter !== null) {
$lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value);
} else {
$lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label);
}
$index++;
}
if (isset($options['unselect'])) {
// add a hidden field so that if the list box has no option being selected, it still submits a value
$name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name;
$hidden = static::hiddenInput($name2, $options['unselect']);
} else {
$hidden = '';
}
$separator = isset($options['separator']) ? $options['separator'] : "\n";
return $hidden . implode($separator, $lines);
}
/**
* Generates a list of radio buttons.
* A radio button list is like a checkbox list, except that it only allows single selection.
* @param string $name the name attribute of each radio button.
* @param string|array $selection the selected value(s).
* @param array $items the data item used to generate the radio buttons.
* The array keys are the labels, while the array values are the corresponding radio button values.
* Note that the labels will NOT be HTML-encoded, while the values will.
* @param array $options options (name => config) for the radio button list. The following options are supported:
*
* - unselect: string, the value that should be submitted when none of the radio buttons is selected.
* By setting this option, a hidden input will be generated.
* - separator: string, the HTML code that separates items.
* - item: callable, a callback that can be used to customize the generation of the HTML code
* corresponding to a single item in $items. The signature of this callback must be:
*
* ~~~
* function ($index, $label, $name, $checked, $value)
* ~~~
*
* where $index is the zero-based index of the radio button in the whole list; $label
* is the label for the radio button; and $name, $value and $checked represent the name,
* value and the checked status of the radio button input.
* @return string the generated radio button list
*/
public static function radioList($name, $selection = null, $items = array(), $options = array())
{
$formatter = isset($options['item']) ? $options['item'] : null;
$lines = array();
$index = 0;
foreach ($items as $value => $label) {
$checked = $selection !== null &&
(!is_array($selection) && !strcmp($value, $selection)
|| is_array($selection) && in_array($value, $selection));
if ($formatter !== null) {
$lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value);
} else {
$lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label);
}
$index++;
}
$separator = isset($options['separator']) ? $options['separator'] : "\n";
if (isset($options['unselect'])) {
// add a hidden field so that if the list box has no option being selected, it still submits a value
$hidden = static::hiddenInput($name, $options['unselect']);
} else {
$hidden = '';
}
return $hidden . implode($separator, $lines);
}
/**
* Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]].
* @param string|array $selection the selected value(s). This can be either a string for single selection
* or an array for multiple selections.
* @param array $items the option data items. The array keys are option values, and the array values
* are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
* For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
* If you have a list of data models, you may convert them into the format described above using
* [[\yii\helpers\ArrayHelper::map()]].
*
* Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in
* the labels will also be HTML-encoded.
* @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call.
* This method will take out these elements, if any: "prompt", "options" and "groups". See more details
* in [[dropDownList()]] for the explanation of these elements.
*
* @return string the generated list options
*/
public static function renderSelectOptions($selection, $items, &$tagOptions = array())
{
$lines = array();
if (isset($tagOptions['prompt'])) {
$prompt = str_replace(' ', '&nbsp;', static::encode($tagOptions['prompt']));
$lines[] = static::tag('option', $prompt, array('value' => ''));
}
$options = isset($tagOptions['options']) ? $tagOptions['options'] : array();
$groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array();
unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']);
foreach ($items as $key => $value) {
if (is_array($value)) {
$groupAttrs = isset($groups[$key]) ? $groups[$key] : array();
$groupAttrs['label'] = $key;
$attrs = array('options' => $options, 'groups' => $groups);
$content = static::renderSelectOptions($selection, $value, $attrs);
$lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs);
} else {
$attrs = isset($options[$key]) ? $options[$key] : array();
$attrs['value'] = $key;
$attrs['selected'] = $selection !== null &&
(!is_array($selection) && !strcmp($key, $selection)
|| is_array($selection) && in_array($key, $selection));
$lines[] = static::tag('option', str_replace(' ', '&nbsp;', static::encode($value)), $attrs);
}
}
return implode("\n", $lines);
}
/**
* Renders the HTML tag attributes.
* Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially
* according to [[booleanAttributes]] and [[showBooleanAttributeValues]].
* @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]].
* Attributes whose value is null will be ignored and not put in the rendering result.
* @return string the rendering result. If the attributes are not empty, they will be rendered
* into a string with a leading white space (such that it can be directly appended to the tag name
* in a tag. If there is no attribute, an empty string will be returned.
*/
public static function renderTagAttributes($attributes)
{
if (count($attributes) > 1) {
$sorted = array();
foreach (static::$attributeOrder as $name) {
if (isset($attributes[$name])) {
$sorted[$name] = $attributes[$name];
}
}
$attributes = array_merge($sorted, $attributes);
}
$html = '';
foreach ($attributes as $name => $value) {
if (isset(static::$booleanAttributes[strtolower($name)])) {
if ($value || strcasecmp($name, $value) === 0) {
$html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name";
}
} elseif ($value !== null) {
$html .= " $name=\"" . static::encode($value) . '"';
}
}
return $html;
}
/**
* Normalizes the input parameter to be a valid URL.
*
* If the input parameter
*
* - is an empty string: the currently requested URL will be returned;
* - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned;
* - is an array: the first array element is considered a route, while the rest of the name-value
* pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]].
* For example: `array('post/index', 'page' => 2)`, `array('index')`.
*
* @param array|string $url the parameter to be used to generate a valid URL
* @return string the normalized URL
* @throws InvalidParamException if the parameter is invalid.
*/
public static function url($url)
{
if (is_array($url)) {
if (isset($url[0])) {
$route = $url[0];
$params = array_splice($url, 1);
if (Yii::$app->controller !== null) {
return Yii::$app->controller->createUrl($route, $params);
} else {
return Yii::$app->getUrlManager()->createUrl($route, $params);
}
} else {
throw new InvalidParamException('The array specifying a URL must contain at least one element.');
}
} elseif ($url === '') {
return Yii::$app->getRequest()->getUrl();
} else {
return Yii::getAlias($url);
}
}
} }

245
framework/helpers/SecurityHelper.php

@ -7,11 +7,6 @@
namespace yii\helpers; namespace yii\helpers;
use Yii;
use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
/** /**
* SecurityHelper provides a set of methods to handle common security-related tasks. * SecurityHelper provides a set of methods to handle common security-related tasks.
* *
@ -29,244 +24,6 @@ use yii\base\InvalidParamException;
* @author Tom Worster <fsb@thefsb.org> * @author Tom Worster <fsb@thefsb.org>
* @since 2.0 * @since 2.0
*/ */
class SecurityHelper class SecurityHelper extends base\SecurityHelper
{
/**
* Encrypts data.
* @param string $data data to be encrypted.
* @param string $key the encryption secret key
* @return string the encrypted data
* @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized
* @see decrypt()
*/
public static function encrypt($data, $key)
{
$module = static::openCryptModule();
$key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module));
srand();
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND);
mcrypt_generic_init($module, $key, $iv);
$encrypted = $iv . mcrypt_generic($module, $data);
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
return $encrypted;
}
/**
* Decrypts data
* @param string $data data to be decrypted.
* @param string $key the decryption secret key
* @return string the decrypted data
* @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized
* @see encrypt()
*/
public static function decrypt($data, $key)
{
$module = static::openCryptModule();
$key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module));
$ivSize = mcrypt_enc_get_iv_size($module);
$iv = StringHelper::substr($data, 0, $ivSize);
mcrypt_generic_init($module, $key, $iv);
$decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data)));
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
return rtrim($decrypted, "\0");
}
/**
* Prefixes data with a keyed hash value so that it can later be detected if it is tampered.
* @param string $data the data to be protected
* @param string $key the secret key to be used for generating hash
* @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()"
* function to see the supported hashing algorithms on your system.
* @return string the data prefixed with the keyed hash
* @see validateData()
* @see getSecretKey()
*/
public static function hashData($data, $key, $algorithm = 'sha256')
{
return hash_hmac($algorithm, $data, $key) . $data;
}
/**
* Validates if the given data is tampered.
* @param string $data the data to be validated. The data must be previously
* generated by [[hashData()]].
* @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]].
* @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()"
* function to see the supported hashing algorithms on your system. This must be the same
* as the value passed to [[hashData()]] when generating the hash for the data.
* @return string the real data with the hash stripped off. False if the data is tampered.
* @see hashData()
*/
public static function validateData($data, $key, $algorithm = 'sha256')
{
$hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key));
$n = StringHelper::strlen($data);
if ($n >= $hashSize) {
$hash = StringHelper::substr($data, 0, $hashSize);
$data2 = StringHelper::substr($data, $hashSize, $n - $hashSize);
return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false;
} else {
return false;
}
}
/**
* Returns a secret key associated with the specified name.
* If the secret key does not exist, a random key will be generated
* and saved in the file "keys.php" under the application's runtime directory
* so that the same secret key can be returned in future requests.
* @param string $name the name that is associated with the secret key
* @param integer $length the length of the key that should be generated if not exists
* @return string the secret key associated with the specified name
*/
public static function getSecretKey($name, $length = 32)
{
static $keys;
$keyFile = Yii::$app->getRuntimePath() . '/keys.php';
if ($keys === null) {
$keys = is_file($keyFile) ? require($keyFile) : array();
}
if (!isset($keys[$name])) {
// generate a 32-char random key
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length);
file_put_contents($keyFile, "<?php\nreturn " . var_export($keys, true) . ";\n");
}
return $keys[$name];
}
/**
* Opens the mcrypt module.
* @return resource the mcrypt module handle.
* @throws InvalidConfigException if mcrypt extension is not installed
* @throws Exception if mcrypt initialization fails
*/
protected static function openCryptModule()
{ {
if (!extension_loaded('mcrypt')) {
throw new InvalidConfigException('The mcrypt PHP extension is not installed.');
}
$module = @mcrypt_module_open('rijndael-256', '', MCRYPT_MODE_CBC, '');
if ($module === false) {
throw new Exception('Failed to initialize the mcrypt module.');
}
return $module;
}
/**
* Generates a secure hash from a password and a random salt.
*
* The generated hash can be stored in database (e.g. `CHAR(64) CHARACTER SET latin1` on MySQL).
* Later when a password needs to be validated, the hash can be fetched and passed
* to [[validatePassword()]]. For example,
*
* ~~~
* // generates the hash (usually done during user registration or when the password is changed)
* $hash = SecurityHelper::hashPassword($password);
* // ...save $hash in database...
*
* // during login, validate if the password entered is correct using $hash fetched from database
* if (PasswordHelper::verifyPassword($password, $hash) {
* // password is good
* } else {
* // password is bad
* }
* ~~~
*
* @param string $password The password to be hashed.
* @param integer $cost Cost parameter used by the Blowfish hash algorithm.
* The higher the value of cost,
* the longer it takes to generate the hash and to verify a password against it. Higher cost
* therefore slows down a brute-force attack. For best protection against brute for attacks,
* set it to the highest value that is tolerable on production servers. The time taken to
* compute the hash doubles for every increment by one of $cost. So, for example, if the
* hash takes 1 second to compute when $cost is 14 then then the compute time varies as
* 2^($cost - 14) seconds.
* @throws Exception on bad password parameter or cost parameter
* @return string The password hash string, ASCII and not longer than 64 characters.
* @see validatePassword()
*/
public static function generatePasswordHash($password, $cost = 13)
{
$salt = static::generateSalt($cost);
$hash = crypt($password, $salt);
if (!is_string($hash) || strlen($hash) < 32) {
throw new Exception('Unknown error occurred while generating hash.');
}
return $hash;
}
/**
* Verifies a password against a hash.
* @param string $password The password to verify.
* @param string $hash The hash to verify the password against.
* @return boolean whether the password is correct.
* @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available.
* @see generatePasswordHash()
*/
public static function validatePassword($password, $hash)
{
if (!is_string($password) || $password === '') {
throw new InvalidParamException('Password must be a string and cannot be empty.');
}
if (!preg_match('/^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) {
throw new InvalidParamException('Hash is invalid.');
}
$test = crypt($password, $hash);
$n = strlen($test);
if (strlen($test) < 32 || $n !== strlen($hash)) {
return false;
}
// Use a for-loop to compare two strings to prevent timing attacks. See:
// http://codereview.stackexchange.com/questions/13512
$check = 0;
for ($i = 0; $i < $n; ++$i) {
$check |= (ord($test[$i]) ^ ord($hash[$i]));
}
return $check === 0;
}
/**
* Generates a salt that can be used to generate a password hash.
*
* The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function
* requires, for the Blowfish hash algorithm, a salt string in a specific format:
* "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters
* from the alphabet "./0-9A-Za-z".
*
* @param integer $cost the cost parameter
* @return string the random salt value.
* @throws InvalidParamException if the cost parameter is not between 4 and 30
*/
protected static function generateSalt($cost = 13)
{
$cost = (int)$cost;
if ($cost < 4 || $cost > 30) {
throw new InvalidParamException('Cost must be between 4 and 31.');
}
// Get 20 * 8bits of pseudo-random entropy from mt_rand().
$rand = '';
for ($i = 0; $i < 20; ++$i) {
$rand .= chr(mt_rand(0, 255));
}
// Add the microtime for a little more entropy.
$rand .= microtime();
// Mix the bits cryptographically into a 20-byte binary string.
$rand = sha1($rand, true);
// Form the prefix that specifies Blowfish algorithm and cost parameter.
$salt = sprintf("$2y$%02d$", $cost);
// Append the random salt data in the required base64 format.
$salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22));
return $salt;
}
} }

108
framework/helpers/StringHelper.php

@ -14,112 +14,6 @@ namespace yii\helpers;
* @author Alex Makarov <sam@rmcreative.ru> * @author Alex Makarov <sam@rmcreative.ru>
* @since 2.0 * @since 2.0
*/ */
class StringHelper class StringHelper extends base\StringHelper
{ {
/**
* Returns the number of bytes in the given string.
* This method ensures the string is treated as a byte array.
* It will use `mb_strlen()` if it is available.
* @param string $string the string being measured for length
* @return integer the number of bytes in the given string.
*/
public static function strlen($string)
{
return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string);
}
/**
* Returns the portion of string specified by the start and length parameters.
* This method ensures the string is treated as a byte array.
* It will use `mb_substr()` if it is available.
* @param string $string the input string. Must be one character or longer.
* @param integer $start the starting position
* @param integer $length the desired portion length
* @return string the extracted part of string, or FALSE on failure or an empty string.
* @see http://www.php.net/manual/en/function.substr.php
*/
public static function substr($string, $start, $length)
{
return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length);
}
/**
* Converts a word to its plural form.
* Note that this is for English only!
* For example, 'apple' will become 'apples', and 'child' will become 'children'.
* @param string $name the word to be pluralized
* @return string the pluralized word
*/
public static function pluralize($name)
{
static $rules = array(
'/(m)ove$/i' => '\1oves',
'/(f)oot$/i' => '\1eet',
'/(c)hild$/i' => '\1hildren',
'/(h)uman$/i' => '\1umans',
'/(m)an$/i' => '\1en',
'/(s)taff$/i' => '\1taff',
'/(t)ooth$/i' => '\1eeth',
'/(p)erson$/i' => '\1eople',
'/([m|l])ouse$/i' => '\1ice',
'/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es',
'/([^aeiouy]|qu)y$/i' => '\1ies',
'/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
'/(shea|lea|loa|thie)f$/i' => '\1ves',
'/([ti])um$/i' => '\1a',
'/(tomat|potat|ech|her|vet)o$/i' => '\1oes',
'/(bu)s$/i' => '\1ses',
'/(ax|test)is$/i' => '\1es',
'/s$/' => 's',
);
foreach ($rules as $rule => $replacement) {
if (preg_match($rule, $name)) {
return preg_replace($rule, $replacement, $name);
}
}
return $name . 's';
}
/**
* Converts a CamelCase name into space-separated words.
* For example, 'PostTag' will be converted to 'Post Tag'.
* @param string $name the string to be converted
* @param boolean $ucwords whether to capitalize the first letter in each word
* @return string the resulting words
*/
public static function camel2words($name, $ucwords = true)
{
$label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $name))));
return $ucwords ? ucwords($label) : $label;
}
/**
* Converts a CamelCase name into an ID in lowercase.
* Words in the ID may be concatenated using the specified character (defaults to '-').
* For example, 'PostTag' will be converted to 'post-tag'.
* @param string $name the string to be converted
* @param string $separator the character used to concatenate the words in the ID
* @return string the resulting ID
*/
public static function camel2id($name, $separator = '-')
{
if ($separator === '_') {
return trim(strtolower(preg_replace('/(?<![A-Z])[A-Z]/', '_\0', $name)), '_');
} else {
return trim(strtolower(str_replace('_', $separator, preg_replace('/(?<![A-Z])[A-Z]/', $separator . '\0', $name))), $separator);
}
}
/**
* Converts an ID into a CamelCase name.
* Words in the ID separated by `$separator` (defaults to '-') will be concatenated into a CamelCase name.
* For example, 'post-tag' is converted to 'PostTag'.
* @param string $id the ID to be converted
* @param string $separator the character used to separate the words in the ID
* @return string the resulting CamelCase name
*/
public static function id2camel($id, $separator = '-')
{
return str_replace(' ', '', ucwords(implode(' ', explode($separator, $id))));
}
} }

108
framework/helpers/VarDumper.php

@ -23,112 +23,6 @@ namespace yii\helpers;
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class CVarDumper class VarDumper extends base\VarDumper
{ {
private static $_objects;
private static $_output;
private static $_depth;
/**
* Displays a variable.
* This method achieves the similar functionality as var_dump and print_r
* but is more robust when handling complex objects such as Yii controllers.
* @param mixed $var variable to be dumped
* @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10.
* @param boolean $highlight whether the result should be syntax-highlighted
*/
public static function dump($var, $depth = 10, $highlight = false)
{
echo self::dumpAsString($var, $depth, $highlight);
}
/**
* Dumps a variable in terms of a string.
* This method achieves the similar functionality as var_dump and print_r
* but is more robust when handling complex objects such as Yii controllers.
* @param mixed $var variable to be dumped
* @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10.
* @param boolean $highlight whether the result should be syntax-highlighted
* @return string the string representation of the variable
*/
public static function dumpAsString($var, $depth = 10, $highlight = false)
{
self::$_output = '';
self::$_objects = array();
self::$_depth = $depth;
self::dumpInternal($var, 0);
if ($highlight) {
$result = highlight_string("<?php\n" . self::$_output, true);
self::$_output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
}
return self::$_output;
}
/*
* @param mixed $var variable to be dumped
* @param integer $level depth level
*/
private static function dumpInternal($var, $level)
{
switch (gettype($var)) {
case 'boolean':
self::$_output .= $var ? 'true' : 'false';
break;
case 'integer':
self::$_output .= "$var";
break;
case 'double':
self::$_output .= "$var";
break;
case 'string':
self::$_output .= "'" . addslashes($var) . "'";
break;
case 'resource':
self::$_output .= '{resource}';
break;
case 'NULL':
self::$_output .= "null";
break;
case 'unknown type':
self::$_output .= '{unknown}';
break;
case 'array':
if (self::$_depth <= $level) {
self::$_output .= 'array(...)';
} elseif (empty($var)) {
self::$_output .= 'array()';
} else {
$keys = array_keys($var);
$spaces = str_repeat(' ', $level * 4);
self::$_output .= "array\n" . $spaces . '(';
foreach ($keys as $key) {
self::$_output .= "\n" . $spaces . ' ';
self::dumpInternal($key, 0);
self::$_output .= ' => ';
self::dumpInternal($var[$key], $level + 1);
}
self::$_output .= "\n" . $spaces . ')';
}
break;
case 'object':
if (($id = array_search($var, self::$_objects, true)) !== false) {
self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)';
} elseif (self::$_depth <= $level) {
self::$_output .= get_class($var) . '(...)';
} else {
$id = self::$_objects[] = $var;
$className = get_class($var);
$members = (array)$var;
$spaces = str_repeat(' ', $level * 4);
self::$_output .= "$className#$id\n" . $spaces . '(';
foreach ($members as $key => $value) {
$keyDisplay = strtr(trim($key), array("\0" => ':'));
self::$_output .= "\n" . $spaces . " [$keyDisplay] => ";
self::dumpInternal($value, $level + 1);
}
self::$_output .= "\n" . $spaces . ')';
}
break;
}
}
} }

340
framework/helpers/base/ArrayHelper.php

@ -0,0 +1,340 @@
<?php
/**
* @copyright Copyright (c) 2008 Yii Software LLC
* @link http://www.yiiframework.com/
* @license http://www.yiiframework.com/license/
*/
namespace yii\helpers\base;
use Yii;
use yii\base\InvalidParamException;
/**
* ArrayHelper provides additional array functionality you can use in your
* application.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class ArrayHelper
{
/**
* Merges two or more arrays into one recursively.
* If each array has an element with the same string key value, the latter
* will overwrite the former (different from array_merge_recursive).
* Recursive merging will be conducted if both arrays have an element of array
* type and are having the same key.
* For integer-keyed elements, the elements from the latter array will
* be appended to the former array.
* @param array $a array to be merged to
* @param array $b array to be merged from. You can specify additional
* arrays via third argument, fourth argument etc.
* @return array the merged array (the original arrays are not changed.)
*/
public static function merge($a, $b)
{
$args = func_get_args();
$res = array_shift($args);
while ($args !== array()) {
$next = array_shift($args);
foreach ($next as $k => $v) {
if (is_integer($k)) {
isset($res[$k]) ? $res[] = $v : $res[$k] = $v;
} elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
$res[$k] = self::merge($res[$k], $v);
} else {
$res[$k] = $v;
}
}
}
return $res;
}
/**
* Retrieves the value of an array element or object property with the given key or property name.
* If the key does not exist in the array, the default value will be returned instead.
*
* Below are some usage examples,
*
* ~~~
* // working with array
* $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username');
* // working with object
* $username = \yii\helpers\ArrayHelper::getValue($user, 'username');
* // working with anonymous function
* $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) {
* return $user->firstName . ' ' . $user->lastName;
* });
* ~~~
*
* @param array|object $array array or object to extract value from
* @param string|\Closure $key key name of the array element, or property name of the object,
* or an anonymous function returning the value. The anonymous function signature should be:
* `function($array, $defaultValue)`.
* @param mixed $default the default value to be returned if the specified key does not exist
* @return mixed the value of the
*/
public static function getValue($array, $key, $default = null)
{
if ($key instanceof \Closure) {
return $key($array, $default);
} elseif (is_array($array)) {
return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default;
} else {
return $array->$key;
}
}
/**
* Indexes an array according to a specified key.
* The input array should be multidimensional or an array of objects.
*
* The key can be a key name of the sub-array, a property name of object, or an anonymous
* function which returns the key value given an array element.
*
* If a key value is null, the corresponding array element will be discarded and not put in the result.
*
* For example,
*
* ~~~
* $array = array(
* array('id' => '123', 'data' => 'abc'),
* array('id' => '345', 'data' => 'def'),
* );
* $result = ArrayHelper::index($array, 'id');
* // the result is:
* // array(
* // '123' => array('id' => '123', 'data' => 'abc'),
* // '345' => array('id' => '345', 'data' => 'def'),
* // )
*
* // using anonymous function
* $result = ArrayHelper::index($array, function(element) {
* return $element['id'];
* });
* ~~~
*
* @param array $array the array that needs to be indexed
* @param string|\Closure $key the column name or anonymous function whose result will be used to index the array
* @return array the indexed array
*/
public static function index($array, $key)
{
$result = array();
foreach ($array as $element) {
$value = static::getValue($element, $key);
$result[$value] = $element;
}
return $result;
}
/**
* Returns the values of a specified column in an array.
* The input array should be multidimensional or an array of objects.
*
* For example,
*
* ~~~
* $array = array(
* array('id' => '123', 'data' => 'abc'),
* array('id' => '345', 'data' => 'def'),
* );
* $result = ArrayHelper::getColumn($array, 'id');
* // the result is: array( '123', '345')
*
* // using anonymous function
* $result = ArrayHelper::getColumn($array, function(element) {
* return $element['id'];
* });
* ~~~
*
* @param array $array
* @param string|\Closure $name
* @param boolean $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
*/
public static function getColumn($array, $name, $keepKeys = true)
{
$result = array();
if ($keepKeys) {
foreach ($array as $k => $element) {
$result[$k] = static::getValue($element, $name);
}
} else {
foreach ($array as $element) {
$result[] = static::getValue($element, $name);
}
}
return $result;
}
/**
* Builds a map (key-value pairs) from a multidimensional array or an array of objects.
* The `$from` and `$to` parameters specify the key names or property names to set up the map.
* Optionally, one can further group the map according to a grouping field `$group`.
*
* For example,
*
* ~~~
* $array = array(
* array('id' => '123', 'name' => 'aaa', 'class' => 'x'),
* array('id' => '124', 'name' => 'bbb', 'class' => 'x'),
* array('id' => '345', 'name' => 'ccc', 'class' => 'y'),
* );
*
* $result = ArrayHelper::map($array, 'id', 'name');
* // the result is:
* // array(
* // '123' => 'aaa',
* // '124' => 'bbb',
* // '345' => 'ccc',
* // )
*
* $result = ArrayHelper::map($array, 'id', 'name', 'class');
* // the result is:
* // array(
* // 'x' => array(
* // '123' => 'aaa',
* // '124' => 'bbb',
* // ),
* // 'y' => array(
* // '345' => 'ccc',
* // ),
* // )
* ~~~
*
* @param array $array
* @param string|\Closure $from
* @param string|\Closure $to
* @param string|\Closure $group
* @return array
*/
public static function map($array, $from, $to, $group = null)
{
$result = array();
foreach ($array as $element) {
$key = static::getValue($element, $from);
$value = static::getValue($element, $to);
if ($group !== null) {
$result[static::getValue($element, $group)][$key] = $value;
} else {
$result[$key] = $value;
}
}
return $result;
}
/**
* Sorts an array of objects or arrays (with the same structure) by one or several keys.
* @param array $array the array to be sorted. The array will be modified after calling this method.
* @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array
* elements, a property name of the objects, or an anonymous function returning the values for comparison
* purpose. The anonymous function signature should be: `function($item)`.
* To sort by multiple keys, provide an array of keys here.
* @param boolean|array $ascending whether to sort in ascending or descending order. When
* sorting by multiple keys with different ascending orders, use an array of ascending flags.
* @param integer|array $sortFlag the PHP sort flag. Valid values include:
* `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last
* value is for sorting strings in case-insensitive manner. Please refer to
* See [PHP manual](http://php.net/manual/en/function.sort.php) for more details.
* When sorting by multiple keys with different sort flags, use an array of sort flags.
* @throws InvalidParamException if the $ascending or $sortFlag parameters do not have
* correct number of elements as that of $key.
*/
public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR)
{
$keys = is_array($key) ? $key : array($key);
if (empty($keys) || empty($array)) {
return;
}
$n = count($keys);
if (is_scalar($ascending)) {
$ascending = array_fill(0, $n, $ascending);
} elseif (count($ascending) !== $n) {
throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.');
}
if (is_scalar($sortFlag)) {
$sortFlag = array_fill(0, $n, $sortFlag);
} elseif (count($sortFlag) !== $n) {
throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.');
}
$args = array();
foreach ($keys as $i => $key) {
$flag = $sortFlag[$i];
if ($flag == (SORT_STRING | SORT_FLAG_CASE)) {
$flag = SORT_STRING;
$column = array();
foreach (static::getColumn($array, $key) as $k => $value) {
$column[$k] = strtolower($value);
}
$args[] = $column;
} else {
$args[] = static::getColumn($array, $key);
}
$args[] = $ascending[$i] ? SORT_ASC : SORT_DESC;
$args[] = $flag;
}
$args[] = &$array;
call_user_func_array('array_multisort', $args);
}
/**
* Encodes special characters in an array of strings into HTML entities.
* Both the array keys and values will be encoded.
* If a value is an array, this method will also encode it recursively.
* @param array $data data to be encoded
* @param boolean $valuesOnly whether to encode array values only. If false,
* both the array keys and array values will be encoded.
* @param string $charset the charset that the data is using. If not set,
* [[\yii\base\Application::charset]] will be used.
* @return array the encoded data
* @see http://www.php.net/manual/en/function.htmlspecialchars.php
*/
public static function htmlEncode($data, $valuesOnly = true, $charset = null)
{
if ($charset === null) {
$charset = Yii::$app->charset;
}
$d = array();
foreach ($data as $key => $value) {
if (!$valuesOnly && is_string($key)) {
$key = htmlspecialchars($key, ENT_QUOTES, $charset);
}
if (is_string($value)) {
$d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset);
} elseif (is_array($value)) {
$d[$key] = static::htmlEncode($value, $charset);
}
}
return $d;
}
/**
* Decodes HTML entities into the corresponding characters in an array of strings.
* Both the array keys and values will be decoded.
* If a value is an array, this method will also decode it recursively.
* @param array $data data to be decoded
* @param boolean $valuesOnly whether to decode array values only. If false,
* both the array keys and array values will be decoded.
* @return array the decoded data
* @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php
*/
public static function htmlDecode($data, $valuesOnly = true)
{
$d = array();
foreach ($data as $key => $value) {
if (!$valuesOnly && is_string($key)) {
$key = htmlspecialchars_decode($key, ENT_QUOTES);
}
if (is_string($value)) {
$d[$key] = htmlspecialchars_decode($value, ENT_QUOTES);
} elseif (is_array($value)) {
$d[$key] = static::htmlDecode($value);
}
}
return $d;
}
}

470
framework/helpers/base/ConsoleColor.php

@ -0,0 +1,470 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\helpers\base;
// todo test this on all kinds of terminals, especially windows (check out lib ncurses)
/**
* Console View is the base class for console view components
*
* A console view provides functionality to create rich console application by allowing to format output
* by adding color and font style to it.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ConsoleColor
{
const FG_BLACK = 30;
const FG_RED = 31;
const FG_GREEN = 32;
const FG_YELLOW = 33;
const FG_BLUE = 34;
const FG_PURPLE = 35;
const FG_CYAN = 36;
const FG_GREY = 37;
const BG_BLACK = 40;
const BG_RED = 41;
const BG_GREEN = 42;
const BG_YELLOW = 43;
const BG_BLUE = 44;
const BG_PURPLE = 45;
const BG_CYAN = 46;
const BG_GREY = 47;
const BOLD = 1;
const ITALIC = 3;
const UNDERLINE = 4;
const BLINK = 5;
const NEGATIVE = 7;
const CONCEALED = 8;
const CROSSED_OUT = 9;
const FRAMED = 51;
const ENCIRCLED = 52;
const OVERLINED = 53;
/**
* Moves the terminal cursor up by sending ANSI control code CUU to the terminal.
* If the cursor is already at the edge of the screen, this has no effect.
* @param integer $rows number of rows the cursor should be moved up
*/
public static function moveCursorUp($rows=1)
{
echo "\033[" . (int) $rows . 'A';
}
/**
* Moves the terminal cursor down by sending ANSI control code CUD to the terminal.
* If the cursor is already at the edge of the screen, this has no effect.
* @param integer $rows number of rows the cursor should be moved down
*/
public static function moveCursorDown($rows=1)
{
echo "\033[" . (int) $rows . 'B';
}
/**
* Moves the terminal cursor forward by sending ANSI control code CUF to the terminal.
* If the cursor is already at the edge of the screen, this has no effect.
* @param integer $steps number of steps the cursor should be moved forward
*/
public static function moveCursorForward($steps=1)
{
echo "\033[" . (int) $steps . 'C';
}
/**
* Moves the terminal cursor backward by sending ANSI control code CUB to the terminal.
* If the cursor is already at the edge of the screen, this has no effect.
* @param integer $steps number of steps the cursor should be moved backward
*/
public static function moveCursorBackward($steps=1)
{
echo "\033[" . (int) $steps . 'D';
}
/**
* Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal.
* @param integer $lines number of lines the cursor should be moved down
*/
public static function moveCursorNextLine($lines=1)
{
echo "\033[" . (int) $lines . 'E';
}
/**
* Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal.
* @param integer $lines number of lines the cursor should be moved up
*/
public static function moveCursorPrevLine($lines=1)
{
echo "\033[" . (int) $lines . 'F';
}
/**
* Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal.
* @param integer $column 1-based column number, 1 is the left edge of the screen.
* @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line.
*/
public static function moveCursorTo($column, $row=null)
{
if ($row === null) {
echo "\033[" . (int) $column . 'G';
} else {
echo "\033[" . (int) $row . ';' . (int) $column . 'H';
}
}
/**
* Scrolls whole page up by sending ANSI control code SU to the terminal.
* New lines are added at the bottom. This is not supported by ANSI.SYS used in windows.
* @param int $lines number of lines to scroll up
*/
public static function scrollUp($lines=1)
{
echo "\033[".(int)$lines."S";
}
/**
* Scrolls whole page down by sending ANSI control code SD to the terminal.
* New lines are added at the top. This is not supported by ANSI.SYS used in windows.
* @param int $lines number of lines to scroll down
*/
public static function scrollDown($lines=1)
{
echo "\033[".(int)$lines."T";
}
/**
* Saves the current cursor position by sending ANSI control code SCP to the terminal.
* Position can then be restored with {@link restoreCursorPosition}.
*/
public static function saveCursorPosition()
{
echo "\033[s";
}
/**
* Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal.
*/
public static function restoreCursorPosition()
{
echo "\033[u";
}
/**
* Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal.
* Use {@link showCursor} to bring it back.
* Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit.
*/
public static function hideCursor()
{
echo "\033[?25l";
}
/**
* Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal.
*/
public static function showCursor()
{
echo "\033[?25h";
}
/**
* Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal.
* Cursor position will not be changed.
* **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen.
*/
public static function clearScreen()
{
echo "\033[2J";
}
/**
* Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal.
* Cursor position will not be changed.
*/
public static function clearScreenBeforeCursor()
{
echo "\033[1J";
}
/**
* Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal.
* Cursor position will not be changed.
*/
public static function clearScreenAfterCursor()
{
echo "\033[0J";
}
/**
* Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal.
* Cursor position will not be changed.
*/
public static function clearLine()
{
echo "\033[2K";
}
/**
* Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal.
* Cursor position will not be changed.
*/
public static function clearLineBeforeCursor()
{
echo "\033[1K";
}
/**
* Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal.
* Cursor position will not be changed.
*/
public static function clearLineAfterCursor()
{
echo "\033[0K";
}
/**
* Will send ANSI format for following output
*
* You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg
* TODO: documentation
*/
public static function ansiStyle()
{
echo "\033[" . implode(';', func_get_args()) . 'm';
}
/**
* Will return a string formatted with the given ANSI style
*
* See {@link ansiStyle} for possible arguments.
* @param string $string the string to be formatted
* @return string
*/
public static function ansiStyleString($string)
{
$args = func_get_args();
array_shift($args);
$code = implode(';', $args);
return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m";
}
//const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors
public static function xterm256ColorFg($i) // TODO naming!
{
return '38;5;'.$i;
}
public static function xterm256ColorBg($i) // TODO naming!
{
return '48;5;'.$i;
}
/**
* Usage: list($w, $h) = ConsoleHelper::getScreenSize();
*
* @return array
*/
public static function getScreenSize()
{
// TODO implement
return array(150,50);
}
/**
* resets any ansi style set by previous method {@link ansiStyle}
* Any output after this is will have default text style.
*/
public static function reset()
{
echo "\033[0m";
}
/**
* Strips ANSI control codes from a string
*
* @param string $string String to strip
* @return string
*/
public static function strip($string)
{
return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color
}
// TODO refactor and review
public static function ansiToHtml($string)
{
$tags = 0;
return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) {
$styleA = array();
foreach(explode(';', $ansi) as $controlCode)
{
switch($controlCode)
{
case static::FG_BLACK: $style = array('color' => '#000000'); break;
case static::FG_BLUE: $style = array('color' => '#000078'); break;
case static::FG_CYAN: $style = array('color' => '#007878'); break;
case static::FG_GREEN: $style = array('color' => '#007800'); break;
case static::FG_GREY: $style = array('color' => '#787878'); break;
case static::FG_PURPLE: $style = array('color' => '#780078'); break;
case static::FG_RED: $style = array('color' => '#780000'); break;
case static::FG_YELLOW: $style = array('color' => '#787800'); break;
case static::BG_BLACK: $style = array('background-color' => '#000000'); break;
case static::BG_BLUE: $style = array('background-color' => '#000078'); break;
case static::BG_CYAN: $style = array('background-color' => '#007878'); break;
case static::BG_GREEN: $style = array('background-color' => '#007800'); break;
case static::BG_GREY: $style = array('background-color' => '#787878'); break;
case static::BG_PURPLE: $style = array('background-color' => '#780078'); break;
case static::BG_RED: $style = array('background-color' => '#780000'); break;
case static::BG_YELLOW: $style = array('background-color' => '#787800'); break;
case static::BOLD: $style = array('font-weight' => 'bold'); break;
case static::ITALIC: $style = array('font-style' => 'italic'); break;
case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break;
case static::OVERLINED: $style = array('text-decoration' => array('overline')); break;
case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break;
case static::BLINK: $style = array('text-decoration' => array('blink')); break;
case static::NEGATIVE: // ???
case static::CONCEALED:
case static::ENCIRCLED:
case static::FRAMED:
// TODO allow resetting codes
break;
case 0: // ansi reset
$return = '';
for($n=$tags; $tags>0; $tags--) {
$return .= '</span>';
}
return $return;
}
$styleA = ArrayHelper::merge($styleA, $style);
}
$styleString[] = array();
foreach($styleA as $name => $content) {
if ($name === 'text-decoration') {
$content = implode(' ', $content);
}
$styleString[] = $name.':'.$content;
}
$tags++;
return '<span' . (!empty($styleString) ? 'style="' . implode(';', $styleString) : '') . '>';
}, $string);
}
/**
* TODO syntax copied from https://github.com/pear/Console_Color2/blob/master/Console/Color2.php
*
* Converts colorcodes in the format %y (for yellow) into ansi-control
* codes. The conversion table is: ('bold' meaning 'light' on some
* terminals). It's almost the same conversion table irssi uses.
* <pre>
* text text background
* ------------------------------------------------
* %k %K %0 black dark grey black
* %r %R %1 red bold red red
* %g %G %2 green bold green green
* %y %Y %3 yellow bold yellow yellow
* %b %B %4 blue bold blue blue
* %m %M %5 magenta bold magenta magenta
* %p %P magenta (think: purple)
* %c %C %6 cyan bold cyan cyan
* %w %W %7 white bold white white
*
* %F Blinking, Flashing
* %U Underline
* %8 Reverse
* %_,%9 Bold
*
* %n Resets the color
* %% A single %
* </pre>
* First param is the string to convert, second is an optional flag if
* colors should be used. It defaults to true, if set to false, the
* colorcodes will just be removed (And %% will be transformed into %)
*
* @param string $string String to convert
* @param bool $colored Should the string be colored?
*
* @return string
*/
public static function renderColoredString($string)
{
$colored = true;
static $conversions = array ( // static so the array doesn't get built
// everytime
// %y - yellow, and so on... {{{
'%y' => array('color' => 'yellow'),
'%g' => array('color' => 'green' ),
'%b' => array('color' => 'blue' ),
'%r' => array('color' => 'red' ),
'%p' => array('color' => 'purple'),
'%m' => array('color' => 'purple'),
'%c' => array('color' => 'cyan' ),
'%w' => array('color' => 'grey' ),
'%k' => array('color' => 'black' ),
'%n' => array('color' => 'reset' ),
'%Y' => array('color' => 'yellow', 'style' => 'light'),
'%G' => array('color' => 'green', 'style' => 'light'),
'%B' => array('color' => 'blue', 'style' => 'light'),
'%R' => array('color' => 'red', 'style' => 'light'),
'%P' => array('color' => 'purple', 'style' => 'light'),
'%M' => array('color' => 'purple', 'style' => 'light'),
'%C' => array('color' => 'cyan', 'style' => 'light'),
'%W' => array('color' => 'grey', 'style' => 'light'),
'%K' => array('color' => 'black', 'style' => 'light'),
'%N' => array('color' => 'reset', 'style' => 'light'),
'%3' => array('background' => 'yellow'),
'%2' => array('background' => 'green' ),
'%4' => array('background' => 'blue' ),
'%1' => array('background' => 'red' ),
'%5' => array('background' => 'purple'),
'%6' => array('background' => 'cyan' ),
'%7' => array('background' => 'grey' ),
'%0' => array('background' => 'black' ),
// Don't use this, I can't stand flashing text
'%F' => array('style' => 'blink'),
'%U' => array('style' => 'underline'),
'%8' => array('style' => 'inverse'),
'%9' => array('style' => 'bold'),
'%_' => array('style' => 'bold')
// }}}
);
if ($colored) {
$string = str_replace('%%', '% ', $string);
foreach ($conversions as $key => $value) {
$string = str_replace($key, Console_Color::color($value),
$string);
}
$string = str_replace('% ', '%', $string);
} else {
$string = preg_replace('/%((%)|.)/', '$2', $string);
}
return $string;
}
/**
* Escapes % so they don't get interpreted as color codes
*
* @param string $string String to escape
*
* @access public
* @return string
*/
public static function escape($string)
{
return str_replace('%', '%%', $string);
}
}

172
framework/helpers/base/FileHelper.php

@ -0,0 +1,172 @@
<?php
/**
* Filesystem helper class file.
*
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\helpers\base;
use Yii;
/**
* Filesystem helper
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Alex Makarov <sam@rmcreative.ru>
* @since 2.0
*/
class FileHelper
{
/**
* Normalizes a file/directory path.
* After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`,
* and any trailing directory separators will be removed. For example, '/home\demo/' on Linux
* will be normalized as '/home/demo'.
* @param string $path the file/directory path to be normalized
* @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`.
* @return string the normalized file/directory path
*/
public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR)
{
return rtrim(strtr($path, array('/' => $ds, '\\' => $ds)), $ds);
}
/**
* Returns the localized version of a specified file.
*
* The searching is based on the specified language code. In particular,
* a file with the same name will be looked for under the subdirectory
* whose name is the same as the language code. For example, given the file "path/to/view.php"
* and language code "zh_CN", the localized file will be looked for as
* "path/to/zh_CN/view.php". If the file is not found, the original file
* will be returned.
*
* If the target and the source language codes are the same,
* the original file will be returned.
*
* @param string $file the original file
* @param string $language the target language that the file should be localized to.
* If not set, the value of [[\yii\base\Application::language]] will be used.
* @param string $sourceLanguage the language that the original file is in.
* If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used.
* @return string the matching localized file, or the original file if the localized version is not found.
* If the target and the source language codes are the same, the original file will be returned.
*/
public static function localize($file, $language = null, $sourceLanguage = null)
{
if ($language === null) {
$language = Yii::$app->language;
}
if ($sourceLanguage === null) {
$sourceLanguage = Yii::$app->sourceLanguage;
}
if ($language === $sourceLanguage) {
return $file;
}
$desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $sourceLanguage . DIRECTORY_SEPARATOR . basename($file);
return is_file($desiredFile) ? $desiredFile : $file;
}
/**
* Determines the MIME type of the specified file.
* This method will first try to determine the MIME type based on
* [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will
* fall back to [[getMimeTypeByExtension()]].
* @param string $file the file name.
* @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`.
* This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php).
* @param boolean $checkExtension whether to use the file extension to determine the MIME type in case
* `finfo_open()` cannot determine it.
* @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined.
*/
public static function getMimeType($file, $magicFile = null, $checkExtension = true)
{
if (function_exists('finfo_open')) {
$info = finfo_open(FILEINFO_MIME_TYPE, $magicFile);
if ($info) {
$result = finfo_file($info, $file);
finfo_close($info);
if ($result !== false) {
return $result;
}
}
}
return $checkExtension ? self::getMimeTypeByExtension($file) : null;
}
/**
* Determines the MIME type based on the extension name of the specified file.
* This method will use a local map between extension names and MIME types.
* @param string $file the file name.
* @param string $magicFile the path of the file that contains all available MIME type information.
* If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used.
* @return string the MIME type. Null is returned if the MIME type cannot be determined.
*/
public static function getMimeTypeByExtension($file, $magicFile = null)
{
static $mimeTypes = array();
if ($magicFile === null) {
$magicFile = __DIR__ . '/mimeTypes.php';
}
if (!isset($mimeTypes[$magicFile])) {
$mimeTypes[$magicFile] = require($magicFile);
}
if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') {
$ext = strtolower($ext);
if (isset($mimeTypes[$magicFile][$ext])) {
return $mimeTypes[$magicFile][$ext];
}
}
return null;
}
/**
* Copies a whole directory as another one.
* The files and sub-directories will also be copied over.
* @param string $src the source directory
* @param string $dst the destination directory
* @param array $options options for directory copy. Valid options are:
*
* - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0777.
* - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting.
* - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
* If the callback returns false, the copy operation for the sub-directory or file will be cancelled.
* The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or
* file to be copied from, while `$to` is the copy target.
* - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied.
* The signature of the callback is similar to that of `beforeCopy`.
*/
public static function copyDirectory($src, $dst, $options = array())
{
if (!is_dir($dst)) {
mkdir($dst, isset($options['dirMode']) ? $options['dirMode'] : 0777, true);
}
$handle = opendir($src);
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$from = $src . DIRECTORY_SEPARATOR . $file;
$to = $dst . DIRECTORY_SEPARATOR . $file;
if (!isset($options['beforeCopy']) || call_user_func($options['beforeCopy'], $from, $to)) {
if (is_file($from)) {
copy($from, $to);
if (isset($options['fileMode'])) {
@chmod($to, $options['fileMode']);
}
} else {
static::copyDirectory($from, $to, $options);
}
if (isset($options['afterCopy'])) {
call_user_func($options['afterCopy'], $from, $to);
}
}
}
closedir($handle);
}
}

981
framework/helpers/base/Html.php

@ -0,0 +1,981 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\helpers\base;
use Yii;
use yii\base\InvalidParamException;
/**
* Html provides a set of static methods for generating commonly used HTML tags.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Html
{
/**
* @var boolean whether to close void (empty) elements. Defaults to true.
* @see voidElements
*/
public static $closeVoidElements = true;
/**
* @var array list of void elements (element name => 1)
* @see closeVoidElements
* @see http://www.w3.org/TR/html-markup/syntax.html#void-element
*/
public static $voidElements = array(
'area' => 1,
'base' => 1,
'br' => 1,
'col' => 1,
'command' => 1,
'embed' => 1,
'hr' => 1,
'img' => 1,
'input' => 1,
'keygen' => 1,
'link' => 1,
'meta' => 1,
'param' => 1,
'source' => 1,
'track' => 1,
'wbr' => 1,
);
/**
* @var boolean whether to show the values of boolean attributes in element tags.
* If false, only the attribute names will be generated.
* @see booleanAttributes
*/
public static $showBooleanAttributeValues = true;
/**
* @var array list of boolean attributes. The presence of a boolean attribute on
* an element represents the true value, and the absence of the attribute represents the false value.
* @see showBooleanAttributeValues
* @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes
*/
public static $booleanAttributes = array(
'async' => 1,
'autofocus' => 1,
'autoplay' => 1,
'checked' => 1,
'controls' => 1,
'declare' => 1,
'default' => 1,
'defer' => 1,
'disabled' => 1,
'formnovalidate' => 1,
'hidden' => 1,
'ismap' => 1,
'loop' => 1,
'multiple' => 1,
'muted' => 1,
'nohref' => 1,
'noresize' => 1,
'novalidate' => 1,
'open' => 1,
'readonly' => 1,
'required' => 1,
'reversed' => 1,
'scoped' => 1,
'seamless' => 1,
'selected' => 1,
'typemustmatch' => 1,
);
/**
* @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes
* that are rendered by [[renderAttributes()]].
*/
public static $attributeOrder = array(
'type',
'id',
'class',
'name',
'value',
'href',
'src',
'action',
'method',
'selected',
'checked',
'readonly',
'disabled',
'multiple',
'size',
'maxlength',
'width',
'height',
'rows',
'cols',
'alt',
'title',
'rel',
'media',
);
/**
* Encodes special characters into HTML entities.
* The [[yii\base\Application::charset|application charset]] will be used for encoding.
* @param string $content the content to be encoded
* @return string the encoded content
* @see decode
* @see http://www.php.net/manual/en/function.htmlspecialchars.php
*/
public static function encode($content)
{
return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset);
}
/**
* Decodes special HTML entities back to the corresponding characters.
* This is the opposite of [[encode()]].
* @param string $content the content to be decoded
* @return string the decoded content
* @see encode
* @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php
*/
public static function decode($content)
{
return htmlspecialchars_decode($content, ENT_QUOTES);
}
/**
* Generates a complete HTML tag.
* @param string $name the tag name
* @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded.
* If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks.
* @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.
* @return string the generated HTML tag
* @see beginTag
* @see endTag
*/
public static function tag($name, $content = '', $options = array())
{
$html = '<' . $name . static::renderTagAttributes($options);
if (isset(static::$voidElements[strtolower($name)])) {
return $html . (static::$closeVoidElements ? ' />' : '>');
} else {
return $html . ">$content</$name>";
}
}
/**
* Generates a start tag.
* @param string $name the tag name
* @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.
* @return string the generated start tag
* @see endTag
* @see tag
*/
public static function beginTag($name, $options = array())
{
return '<' . $name . static::renderTagAttributes($options) . '>';
}
/**
* Generates an end tag.
* @param string $name the tag name
* @return string the generated end tag
* @see beginTag
* @see tag
*/
public static function endTag($name)
{
return "</$name>";
}
/**
* Encloses the given content within a CDATA tag.
* @param string $content the content to be enclosed within the CDATA tag
* @return string the CDATA tag with the enclosed content.
*/
public static function cdata($content)
{
return '<![CDATA[' . $content . ']]>';
}
/**
* Generates a style tag.
* @param string $content the style content
* @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.
* If the options does not contain "type", a "type" attribute with value "text/css" will be used.
* @return string the generated style tag
*/
public static function style($content, $options = array())
{
if (!isset($options['type'])) {
$options['type'] = 'text/css';
}
return static::tag('style', "/*<![CDATA[*/\n{$content}\n/*]]>*/", $options);
}
/**
* Generates a script tag.
* @param string $content the script content
* @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.
* If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered.
* @return string the generated script tag
*/
public static function script($content, $options = array())
{
if (!isset($options['type'])) {
$options['type'] = 'text/javascript';
}
return static::tag('script', "/*<![CDATA[*/\n{$content}\n/*]]>*/", $options);
}
/**
* Generates a link tag that refers to an external CSS file.
* @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]].
* @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.
* @return string the generated link tag
* @see url
*/
public static function cssFile($url, $options = array())
{
$options['rel'] = 'stylesheet';
$options['type'] = 'text/css';
$options['href'] = static::url($url);
return static::tag('link', '', $options);
}
/**
* Generates a script tag that refers to an external JavaScript file.
* @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]].
* @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.
* @return string the generated script tag
* @see url
*/
public static function jsFile($url, $options = array())
{
$options['type'] = 'text/javascript';
$options['src'] = static::url($url);
return static::tag('script', '', $options);
}
/**
* Generates a form start tag.
* @param array|string $action the form action URL. This parameter will be processed by [[url()]].
* @param string $method the form submission method, either "post" or "get" (case-insensitive)
* @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.
* @return string the generated form start tag.
* @see endForm
*/
public static function beginForm($action = '', $method = 'post', $options = array())
{
$action = static::url($action);
// query parameters in the action are ignored for GET method
// we use hidden fields to add them back
$hiddens = array();
if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) {
foreach (explode('&', substr($action, $pos + 1)) as $pair) {
if (($pos1 = strpos($pair, '=')) !== false) {
$hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1)));
} else {
$hiddens[] = static::hiddenInput(urldecode($pair), '');
}
}
$action = substr($action, 0, $pos);
}
$options['action'] = $action;
$options['method'] = $method;
$form = static::beginTag('form', $options);
if ($hiddens !== array()) {
$form .= "\n" . implode("\n", $hiddens);
}
return $form;
}
/**
* Generates a form end tag.
* @return string the generated tag
* @see beginForm
*/
public static function endForm()
{
return '</form>';
}
/**
* Generates a hyperlink tag.
* @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 is coming from end users, you should consider [[encode()]]
* it to prevent XSS attacks.
* @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]]
* and will be used for the "href" attribute of the tag. If this parameter is null, the "href" 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.
* @return string the generated hyperlink
* @see url
*/
public static function a($text, $url = null, $options = array())
{
if ($url !== null) {
$options['href'] = static::url($url);
}
return static::tag('a', $text, $options);
}
/**
* Generates a mailto hyperlink.
* @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 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
* 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()]].
* If a value is null, the corresponding attribute will not be rendered.
* @return string the generated mailto link
*/
public static function mailto($text, $email = null, $options = array())
{
return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options);
}
/**
* Generates an image tag.
* @param string $src the image URL. This parameter will be processed by [[url()]].
* @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.
* @return string the generated image tag
*/
public static function img($src, $options = array())
{
$options['src'] = static::url($src);
if (!isset($options['alt'])) {
$options['alt'] = '';
}
return static::tag('img', null, $options);
}
/**
* Generates a label tag.
* @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 consider [[encode()]]
* it to prevent XSS attacks.
* @param string $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()]].
* If a value is null, the corresponding attribute will not be rendered.
* @return string the generated label tag
*/
public static function label($content, $for = null, $options = array())
{
$options['for'] = $for;
return static::tag('label', $content, $options);
}
/**
* Generates a button tag.
* @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 $content the content enclosed within the button tag. 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 consider [[encode()]] it to prevent XSS attacks.
* @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.
* If the options does not contain "type", a "type" attribute with value "button" will be rendered.
* @return string the generated button tag
*/
public static function button($name = null, $value = null, $content = 'Button', $options = array())
{
$options['name'] = $name;
$options['value'] = $value;
if (!isset($options['type'])) {
$options['type'] = 'button';
}
return static::tag('button', $content, $options);
}
/**
* Generates a submit button tag.
* @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 $content the content enclosed within the button tag. 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 consider [[encode()]] it to prevent XSS attacks.
* @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.
* @return string the generated submit button tag
*/
public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array())
{
$options['type'] = 'submit';
return static::button($name, $value, $content, $options);
}
/**
* Generates a reset button tag.
* @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 $content the content enclosed within the button tag. 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 consider [[encode()]] it to prevent XSS attacks.
* @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.
* @return string the generated reset button tag
*/
public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array())
{
$options['type'] = 'reset';
return static::button($name, $value, $content, $options);
}
/**
* 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 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.
* @return string the generated input tag
*/
public static function input($type, $name = null, $value = null, $options = array())
{
$options['type'] = $type;
$options['name'] = $name;
$options['value'] = $value;
return static::tag('input', null, $options);
}
/**
* Generates an input button.
* @param string $name the name attribute.
* @param string $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.
* @return string the generated button tag
*/
public static function buttonInput($name, $value = 'Button', $options = array())
{
return static::input('button', $name, $value, $options);
}
/**
* Generates a submit input button.
* @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 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.
* @return string the generated button tag
*/
public static function submitInput($name = null, $value = 'Submit', $options = array())
{
return static::input('submit', $name, $value, $options);
}
/**
* Generates a reset input button.
* @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 array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]].
* Attributes whose value is null will be ignored and not put in the tag returned.
* @return string the generated button tag
*/
public static function resetInput($name = null, $value = 'Reset', $options = array())
{
return static::input('reset', $name, $value, $options);
}
/**
* 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 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.
* @return string the generated button tag
*/
public static function textInput($name, $value = null, $options = array())
{
return static::input('text', $name, $value, $options);
}
/**
* 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 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.
* @return string the generated button tag
*/
public static function hiddenInput($name, $value = null, $options = array())
{
return static::input('hidden', $name, $value, $options);
}
/**
* 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 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.
* @return string the generated button tag
*/
public static function passwordInput($name, $value = null, $options = array())
{
return static::input('password', $name, $value, $options);
}
/**
* Generates a file input field.
* To use a file input field, you should set the enclosing form's "enctype" attribute to
* 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 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.
* @return string the generated button tag
*/
public static function fileInput($name, $value = null, $options = array())
{
return static::input('file', $name, $value, $options);
}
/**
* Generates a text area input.
* @param string $name the input name
* @param string $value the input value. Note that it will be encoded using [[encode()]].
* @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.
* @return string the generated text area tag
*/
public static function textarea($name, $value = '', $options = array())
{
$options['name'] = $name;
return static::tag('textarea', static::encode($value), $options);
}
/**
* Generates a radio button input.
* @param string $name the name attribute.
* @param boolean $checked whether the radio button should be checked.
* @param string $value the value attribute. If it is null, the value attribute will not be rendered.
* @param array $options the tag options in terms of name-value pairs. The following options are supported:
*
* - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute
* is present, a hidden input will be generated so that if the radio button is not checked and is submitted,
* the value of this attribute will still be submitted to the server via the hidden input.
*
* 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.
*
* @return string the generated radio button tag
*/
public static function radio($name, $checked = false, $value = '1', $options = array())
{
$options['checked'] = $checked;
$options['value'] = $value;
if (isset($options['uncheck'])) {
// add a hidden field so that if the radio button is not selected, it still submits a value
$hidden = static::hiddenInput($name, $options['uncheck']);
unset($options['uncheck']);
} else {
$hidden = '';
}
return $hidden . static::input('radio', $name, $value, $options);
}
/**
* Generates a checkbox input.
* @param string $name the name attribute.
* @param boolean $checked whether the checkbox should be checked.
* @param string $value the value attribute. If it is null, the value attribute will not be rendered.
* @param array $options the tag options in terms of name-value pairs. The following options are supported:
*
* - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute
* is present, a hidden input will be generated so that if the checkbox is not checked and is submitted,
* the value of this attribute will still be submitted to the server via the hidden input.
*
* 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.
*
* @return string the generated checkbox tag
*/
public static function checkbox($name, $checked = false, $value = '1', $options = array())
{
$options['checked'] = $checked;
$options['value'] = $value;
if (isset($options['uncheck'])) {
// add a hidden field so that if the checkbox is not selected, it still submits a value
$hidden = static::hiddenInput($name, $options['uncheck']);
unset($options['uncheck']);
} else {
$hidden = '';
}
return $hidden . static::input('checkbox', $name, $value, $options);
}
/**
* Generates a drop-down list.
* @param string $name the input name
* @param string $selection the selected value
* @param array $items the option data items. The array keys are option values, and the array values
* are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
* For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
* If you have a list of data models, you may convert them into the format described above using
* [[\yii\helpers\ArrayHelper::map()]].
*
* Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in
* the labels will also be HTML-encoded.
* @param array $options the tag options in terms of name-value pairs. The following options are supported:
*
* - prompt: string, a prompt text to be displayed as the first option;
* - options: array, the attributes for the select option tags. The array keys must be valid option values,
* and the array values are the extra attributes for the corresponding option tags. For example,
*
* ~~~
* array(
* 'value1' => array('disabled' => true),
* 'value2' => array('label' => 'value 2'),
* );
* ~~~
*
* - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options',
* except that the array keys represent the optgroup labels specified in $items.
*
* 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.
*
* @return string the generated drop-down list tag
*/
public static function dropDownList($name, $selection = null, $items = array(), $options = array())
{
$options['name'] = $name;
$selectOptions = static::renderSelectOptions($selection, $items, $options);
return static::tag('select', "\n" . $selectOptions . "\n", $options);
}
/**
* Generates a list box.
* @param string $name the input name
* @param string|array $selection the selected value(s)
* @param array $items the option data items. The array keys are option values, and the array values
* are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
* For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
* If you have a list of data models, you may convert them into the format described above using
* [[\yii\helpers\ArrayHelper::map()]].
*
* Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in
* the labels will also be HTML-encoded.
* @param array $options the tag options in terms of name-value pairs. The following options are supported:
*
* - prompt: string, a prompt text to be displayed as the first option;
* - options: array, the attributes for the select option tags. The array keys must be valid option values,
* and the array values are the extra attributes for the corresponding option tags. For example,
*
* ~~~
* array(
* 'value1' => array('disabled' => true),
* 'value2' => array('label' => 'value 2'),
* );
* ~~~
*
* - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options',
* except that the array keys represent the optgroup labels specified in $items.
* - unselect: string, the value that will be submitted when no option is selected.
* When this attribute is set, a hidden field will be generated so that if no option is selected in multiple
* mode, we can still obtain the posted unselect value.
*
* 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.
*
* @return string the generated list box tag
*/
public static function listBox($name, $selection = null, $items = array(), $options = array())
{
if (!isset($options['size'])) {
$options['size'] = 4;
}
if (!empty($options['multiple']) && substr($name, -2) !== '[]') {
$name .= '[]';
}
$options['name'] = $name;
if (isset($options['unselect'])) {
// add a hidden field so that if the list box has no option being selected, it still submits a value
if (substr($name, -2) === '[]') {
$name = substr($name, 0, -2);
}
$hidden = static::hiddenInput($name, $options['unselect']);
unset($options['unselect']);
} else {
$hidden = '';
}
$selectOptions = static::renderSelectOptions($selection, $items, $options);
return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options);
}
/**
* Generates a list of checkboxes.
* A checkbox list allows multiple selection, like [[listBox()]].
* As a result, the corresponding submitted value is an array.
* @param string $name the name attribute of each checkbox.
* @param string|array $selection the selected value(s).
* @param array $items the data item used to generate the checkboxes.
* The array keys are the labels, while the array values are the corresponding checkbox values.
* Note that the labels will NOT be HTML-encoded, while the values will.
* @param array $options options (name => config) for the checkbox list. The following options are supported:
*
* - unselect: string, the value that should be submitted when none of the checkboxes is selected.
* By setting this option, a hidden input will be generated.
* - separator: string, the HTML code that separates items.
* - item: callable, a callback that can be used to customize the generation of the HTML code
* corresponding to a single item in $items. The signature of this callback must be:
*
* ~~~
* function ($index, $label, $name, $checked, $value)
* ~~~
*
* where $index is the zero-based index of the checkbox in the whole list; $label
* is the label for the checkbox; and $name, $value and $checked represent the name,
* value and the checked status of the checkbox input.
* @return string the generated checkbox list
*/
public static function checkboxList($name, $selection = null, $items = array(), $options = array())
{
if (substr($name, -2) !== '[]') {
$name .= '[]';
}
$formatter = isset($options['item']) ? $options['item'] : null;
$lines = array();
$index = 0;
foreach ($items as $value => $label) {
$checked = $selection !== null &&
(!is_array($selection) && !strcmp($value, $selection)
|| is_array($selection) && in_array($value, $selection));
if ($formatter !== null) {
$lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value);
} else {
$lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label);
}
$index++;
}
if (isset($options['unselect'])) {
// add a hidden field so that if the list box has no option being selected, it still submits a value
$name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name;
$hidden = static::hiddenInput($name2, $options['unselect']);
} else {
$hidden = '';
}
$separator = isset($options['separator']) ? $options['separator'] : "\n";
return $hidden . implode($separator, $lines);
}
/**
* Generates a list of radio buttons.
* A radio button list is like a checkbox list, except that it only allows single selection.
* @param string $name the name attribute of each radio button.
* @param string|array $selection the selected value(s).
* @param array $items the data item used to generate the radio buttons.
* The array keys are the labels, while the array values are the corresponding radio button values.
* Note that the labels will NOT be HTML-encoded, while the values will.
* @param array $options options (name => config) for the radio button list. The following options are supported:
*
* - unselect: string, the value that should be submitted when none of the radio buttons is selected.
* By setting this option, a hidden input will be generated.
* - separator: string, the HTML code that separates items.
* - item: callable, a callback that can be used to customize the generation of the HTML code
* corresponding to a single item in $items. The signature of this callback must be:
*
* ~~~
* function ($index, $label, $name, $checked, $value)
* ~~~
*
* where $index is the zero-based index of the radio button in the whole list; $label
* is the label for the radio button; and $name, $value and $checked represent the name,
* value and the checked status of the radio button input.
* @return string the generated radio button list
*/
public static function radioList($name, $selection = null, $items = array(), $options = array())
{
$formatter = isset($options['item']) ? $options['item'] : null;
$lines = array();
$index = 0;
foreach ($items as $value => $label) {
$checked = $selection !== null &&
(!is_array($selection) && !strcmp($value, $selection)
|| is_array($selection) && in_array($value, $selection));
if ($formatter !== null) {
$lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value);
} else {
$lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label);
}
$index++;
}
$separator = isset($options['separator']) ? $options['separator'] : "\n";
if (isset($options['unselect'])) {
// add a hidden field so that if the list box has no option being selected, it still submits a value
$hidden = static::hiddenInput($name, $options['unselect']);
} else {
$hidden = '';
}
return $hidden . implode($separator, $lines);
}
/**
* Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]].
* @param string|array $selection the selected value(s). This can be either a string for single selection
* or an array for multiple selections.
* @param array $items the option data items. The array keys are option values, and the array values
* are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
* For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
* If you have a list of data models, you may convert them into the format described above using
* [[\yii\helpers\ArrayHelper::map()]].
*
* Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in
* the labels will also be HTML-encoded.
* @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call.
* This method will take out these elements, if any: "prompt", "options" and "groups". See more details
* in [[dropDownList()]] for the explanation of these elements.
*
* @return string the generated list options
*/
public static function renderSelectOptions($selection, $items, &$tagOptions = array())
{
$lines = array();
if (isset($tagOptions['prompt'])) {
$prompt = str_replace(' ', '&nbsp;', static::encode($tagOptions['prompt']));
$lines[] = static::tag('option', $prompt, array('value' => ''));
}
$options = isset($tagOptions['options']) ? $tagOptions['options'] : array();
$groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array();
unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']);
foreach ($items as $key => $value) {
if (is_array($value)) {
$groupAttrs = isset($groups[$key]) ? $groups[$key] : array();
$groupAttrs['label'] = $key;
$attrs = array('options' => $options, 'groups' => $groups);
$content = static::renderSelectOptions($selection, $value, $attrs);
$lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs);
} else {
$attrs = isset($options[$key]) ? $options[$key] : array();
$attrs['value'] = $key;
$attrs['selected'] = $selection !== null &&
(!is_array($selection) && !strcmp($key, $selection)
|| is_array($selection) && in_array($key, $selection));
$lines[] = static::tag('option', str_replace(' ', '&nbsp;', static::encode($value)), $attrs);
}
}
return implode("\n", $lines);
}
/**
* Renders the HTML tag attributes.
* Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially
* according to [[booleanAttributes]] and [[showBooleanAttributeValues]].
* @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]].
* Attributes whose value is null will be ignored and not put in the rendering result.
* @return string the rendering result. If the attributes are not empty, they will be rendered
* into a string with a leading white space (such that it can be directly appended to the tag name
* in a tag. If there is no attribute, an empty string will be returned.
*/
public static function renderTagAttributes($attributes)
{
if (count($attributes) > 1) {
$sorted = array();
foreach (static::$attributeOrder as $name) {
if (isset($attributes[$name])) {
$sorted[$name] = $attributes[$name];
}
}
$attributes = array_merge($sorted, $attributes);
}
$html = '';
foreach ($attributes as $name => $value) {
if (isset(static::$booleanAttributes[strtolower($name)])) {
if ($value || strcasecmp($name, $value) === 0) {
$html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name";
}
} elseif ($value !== null) {
$html .= " $name=\"" . static::encode($value) . '"';
}
}
return $html;
}
/**
* Normalizes the input parameter to be a valid URL.
*
* If the input parameter
*
* - is an empty string: the currently requested URL will be returned;
* - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned;
* - is an array: the first array element is considered a route, while the rest of the name-value
* pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]].
* For example: `array('post/index', 'page' => 2)`, `array('index')`.
*
* @param array|string $url the parameter to be used to generate a valid URL
* @return string the normalized URL
* @throws InvalidParamException if the parameter is invalid.
*/
public static function url($url)
{
if (is_array($url)) {
if (isset($url[0])) {
$route = $url[0];
$params = array_splice($url, 1);
if (Yii::$app->controller !== null) {
return Yii::$app->controller->createUrl($route, $params);
} else {
return Yii::$app->getUrlManager()->createUrl($route, $params);
}
} else {
throw new InvalidParamException('The array specifying a URL must contain at least one element.');
}
} elseif ($url === '') {
return Yii::$app->getRequest()->getUrl();
} else {
return Yii::getAlias($url);
}
}
}

272
framework/helpers/base/SecurityHelper.php

@ -0,0 +1,272 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\helpers\base;
use Yii;
use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
/**
* SecurityHelper provides a set of methods to handle common security-related tasks.
*
* In particular, SecurityHelper supports the following features:
*
* - Encryption/decryption: [[encrypt()]] and [[decrypt()]]
* - Data tampering prevention: [[hashData()]] and [[validateData()]]
* - Password validation: [[generatePasswordHash()]] and [[validatePassword()]]
*
* Additionally, SecurityHelper provides [[getSecretKey()]] to support generating
* named secret keys. These secret keys, once generated, will be stored in a file
* and made available in future requests.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Tom Worster <fsb@thefsb.org>
* @since 2.0
*/
class SecurityHelper
{
/**
* Encrypts data.
* @param string $data data to be encrypted.
* @param string $key the encryption secret key
* @return string the encrypted data
* @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized
* @see decrypt()
*/
public static function encrypt($data, $key)
{
$module = static::openCryptModule();
$key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module));
srand();
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND);
mcrypt_generic_init($module, $key, $iv);
$encrypted = $iv . mcrypt_generic($module, $data);
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
return $encrypted;
}
/**
* Decrypts data
* @param string $data data to be decrypted.
* @param string $key the decryption secret key
* @return string the decrypted data
* @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized
* @see encrypt()
*/
public static function decrypt($data, $key)
{
$module = static::openCryptModule();
$key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module));
$ivSize = mcrypt_enc_get_iv_size($module);
$iv = StringHelper::substr($data, 0, $ivSize);
mcrypt_generic_init($module, $key, $iv);
$decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data)));
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
return rtrim($decrypted, "\0");
}
/**
* Prefixes data with a keyed hash value so that it can later be detected if it is tampered.
* @param string $data the data to be protected
* @param string $key the secret key to be used for generating hash
* @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()"
* function to see the supported hashing algorithms on your system.
* @return string the data prefixed with the keyed hash
* @see validateData()
* @see getSecretKey()
*/
public static function hashData($data, $key, $algorithm = 'sha256')
{
return hash_hmac($algorithm, $data, $key) . $data;
}
/**
* Validates if the given data is tampered.
* @param string $data the data to be validated. The data must be previously
* generated by [[hashData()]].
* @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]].
* @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()"
* function to see the supported hashing algorithms on your system. This must be the same
* as the value passed to [[hashData()]] when generating the hash for the data.
* @return string the real data with the hash stripped off. False if the data is tampered.
* @see hashData()
*/
public static function validateData($data, $key, $algorithm = 'sha256')
{
$hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key));
$n = StringHelper::strlen($data);
if ($n >= $hashSize) {
$hash = StringHelper::substr($data, 0, $hashSize);
$data2 = StringHelper::substr($data, $hashSize, $n - $hashSize);
return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false;
} else {
return false;
}
}
/**
* Returns a secret key associated with the specified name.
* If the secret key does not exist, a random key will be generated
* and saved in the file "keys.php" under the application's runtime directory
* so that the same secret key can be returned in future requests.
* @param string $name the name that is associated with the secret key
* @param integer $length the length of the key that should be generated if not exists
* @return string the secret key associated with the specified name
*/
public static function getSecretKey($name, $length = 32)
{
static $keys;
$keyFile = Yii::$app->getRuntimePath() . '/keys.php';
if ($keys === null) {
$keys = is_file($keyFile) ? require($keyFile) : array();
}
if (!isset($keys[$name])) {
// generate a 32-char random key
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length);
file_put_contents($keyFile, "<?php\nreturn " . var_export($keys, true) . ";\n");
}
return $keys[$name];
}
/**
* Opens the mcrypt module.
* @return resource the mcrypt module handle.
* @throws InvalidConfigException if mcrypt extension is not installed
* @throws Exception if mcrypt initialization fails
*/
protected static function openCryptModule()
{
if (!extension_loaded('mcrypt')) {
throw new InvalidConfigException('The mcrypt PHP extension is not installed.');
}
$module = @mcrypt_module_open('rijndael-256', '', MCRYPT_MODE_CBC, '');
if ($module === false) {
throw new Exception('Failed to initialize the mcrypt module.');
}
return $module;
}
/**
* Generates a secure hash from a password and a random salt.
*
* The generated hash can be stored in database (e.g. `CHAR(64) CHARACTER SET latin1` on MySQL).
* Later when a password needs to be validated, the hash can be fetched and passed
* to [[validatePassword()]]. For example,
*
* ~~~
* // generates the hash (usually done during user registration or when the password is changed)
* $hash = SecurityHelper::hashPassword($password);
* // ...save $hash in database...
*
* // during login, validate if the password entered is correct using $hash fetched from database
* if (PasswordHelper::verifyPassword($password, $hash) {
* // password is good
* } else {
* // password is bad
* }
* ~~~
*
* @param string $password The password to be hashed.
* @param integer $cost Cost parameter used by the Blowfish hash algorithm.
* The higher the value of cost,
* the longer it takes to generate the hash and to verify a password against it. Higher cost
* therefore slows down a brute-force attack. For best protection against brute for attacks,
* set it to the highest value that is tolerable on production servers. The time taken to
* compute the hash doubles for every increment by one of $cost. So, for example, if the
* hash takes 1 second to compute when $cost is 14 then then the compute time varies as
* 2^($cost - 14) seconds.
* @throws Exception on bad password parameter or cost parameter
* @return string The password hash string, ASCII and not longer than 64 characters.
* @see validatePassword()
*/
public static function generatePasswordHash($password, $cost = 13)
{
$salt = static::generateSalt($cost);
$hash = crypt($password, $salt);
if (!is_string($hash) || strlen($hash) < 32) {
throw new Exception('Unknown error occurred while generating hash.');
}
return $hash;
}
/**
* Verifies a password against a hash.
* @param string $password The password to verify.
* @param string $hash The hash to verify the password against.
* @return boolean whether the password is correct.
* @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available.
* @see generatePasswordHash()
*/
public static function validatePassword($password, $hash)
{
if (!is_string($password) || $password === '') {
throw new InvalidParamException('Password must be a string and cannot be empty.');
}
if (!preg_match('/^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) {
throw new InvalidParamException('Hash is invalid.');
}
$test = crypt($password, $hash);
$n = strlen($test);
if (strlen($test) < 32 || $n !== strlen($hash)) {
return false;
}
// Use a for-loop to compare two strings to prevent timing attacks. See:
// http://codereview.stackexchange.com/questions/13512
$check = 0;
for ($i = 0; $i < $n; ++$i) {
$check |= (ord($test[$i]) ^ ord($hash[$i]));
}
return $check === 0;
}
/**
* Generates a salt that can be used to generate a password hash.
*
* The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function
* requires, for the Blowfish hash algorithm, a salt string in a specific format:
* "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters
* from the alphabet "./0-9A-Za-z".
*
* @param integer $cost the cost parameter
* @return string the random salt value.
* @throws InvalidParamException if the cost parameter is not between 4 and 30
*/
protected static function generateSalt($cost = 13)
{
$cost = (int)$cost;
if ($cost < 4 || $cost > 30) {
throw new InvalidParamException('Cost must be between 4 and 31.');
}
// Get 20 * 8bits of pseudo-random entropy from mt_rand().
$rand = '';
for ($i = 0; $i < 20; ++$i) {
$rand .= chr(mt_rand(0, 255));
}
// Add the microtime for a little more entropy.
$rand .= microtime();
// Mix the bits cryptographically into a 20-byte binary string.
$rand = sha1($rand, true);
// Form the prefix that specifies Blowfish algorithm and cost parameter.
$salt = sprintf("$2y$%02d$", $cost);
// Append the random salt data in the required base64 format.
$salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22));
return $salt;
}
}

125
framework/helpers/base/StringHelper.php

@ -0,0 +1,125 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\helpers\base;
/**
* StringHelper
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Alex Makarov <sam@rmcreative.ru>
* @since 2.0
*/
class StringHelper
{
/**
* Returns the number of bytes in the given string.
* This method ensures the string is treated as a byte array.
* It will use `mb_strlen()` if it is available.
* @param string $string the string being measured for length
* @return integer the number of bytes in the given string.
*/
public static function strlen($string)
{
return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string);
}
/**
* Returns the portion of string specified by the start and length parameters.
* This method ensures the string is treated as a byte array.
* It will use `mb_substr()` if it is available.
* @param string $string the input string. Must be one character or longer.
* @param integer $start the starting position
* @param integer $length the desired portion length
* @return string the extracted part of string, or FALSE on failure or an empty string.
* @see http://www.php.net/manual/en/function.substr.php
*/
public static function substr($string, $start, $length)
{
return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length);
}
/**
* Converts a word to its plural form.
* Note that this is for English only!
* For example, 'apple' will become 'apples', and 'child' will become 'children'.
* @param string $name the word to be pluralized
* @return string the pluralized word
*/
public static function pluralize($name)
{
static $rules = array(
'/(m)ove$/i' => '\1oves',
'/(f)oot$/i' => '\1eet',
'/(c)hild$/i' => '\1hildren',
'/(h)uman$/i' => '\1umans',
'/(m)an$/i' => '\1en',
'/(s)taff$/i' => '\1taff',
'/(t)ooth$/i' => '\1eeth',
'/(p)erson$/i' => '\1eople',
'/([m|l])ouse$/i' => '\1ice',
'/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es',
'/([^aeiouy]|qu)y$/i' => '\1ies',
'/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
'/(shea|lea|loa|thie)f$/i' => '\1ves',
'/([ti])um$/i' => '\1a',
'/(tomat|potat|ech|her|vet)o$/i' => '\1oes',
'/(bu)s$/i' => '\1ses',
'/(ax|test)is$/i' => '\1es',
'/s$/' => 's',
);
foreach ($rules as $rule => $replacement) {
if (preg_match($rule, $name)) {
return preg_replace($rule, $replacement, $name);
}
}
return $name . 's';
}
/**
* Converts a CamelCase name into space-separated words.
* For example, 'PostTag' will be converted to 'Post Tag'.
* @param string $name the string to be converted
* @param boolean $ucwords whether to capitalize the first letter in each word
* @return string the resulting words
*/
public static function camel2words($name, $ucwords = true)
{
$label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $name))));
return $ucwords ? ucwords($label) : $label;
}
/**
* Converts a CamelCase name into an ID in lowercase.
* Words in the ID may be concatenated using the specified character (defaults to '-').
* For example, 'PostTag' will be converted to 'post-tag'.
* @param string $name the string to be converted
* @param string $separator the character used to concatenate the words in the ID
* @return string the resulting ID
*/
public static function camel2id($name, $separator = '-')
{
if ($separator === '_') {
return trim(strtolower(preg_replace('/(?<![A-Z])[A-Z]/', '_\0', $name)), '_');
} else {
return trim(strtolower(str_replace('_', $separator, preg_replace('/(?<![A-Z])[A-Z]/', $separator . '\0', $name))), $separator);
}
}
/**
* Converts an ID into a CamelCase name.
* Words in the ID separated by `$separator` (defaults to '-') will be concatenated into a CamelCase name.
* For example, 'post-tag' is converted to 'PostTag'.
* @param string $id the ID to be converted
* @param string $separator the character used to separate the words in the ID
* @return string the resulting CamelCase name
*/
public static function id2camel($id, $separator = '-')
{
return str_replace(' ', '', ucwords(implode(' ', explode($separator, $id))));
}
}

134
framework/helpers/base/VarDumper.php

@ -0,0 +1,134 @@
<?php
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008-2011 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\helpers\base;
/**
* VarDumper is intended to replace the buggy PHP function var_dump and print_r.
* It can correctly identify the recursively referenced objects in a complex
* object structure. It also has a recursive depth control to avoid indefinite
* recursive display of some peculiar variables.
*
* VarDumper can be used as follows,
*
* ~~~
* VarDumper::dump($var);
* ~~~
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class VarDumper
{
private static $_objects;
private static $_output;
private static $_depth;
/**
* Displays a variable.
* This method achieves the similar functionality as var_dump and print_r
* but is more robust when handling complex objects such as Yii controllers.
* @param mixed $var variable to be dumped
* @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10.
* @param boolean $highlight whether the result should be syntax-highlighted
*/
public static function dump($var, $depth = 10, $highlight = false)
{
echo self::dumpAsString($var, $depth, $highlight);
}
/**
* Dumps a variable in terms of a string.
* This method achieves the similar functionality as var_dump and print_r
* but is more robust when handling complex objects such as Yii controllers.
* @param mixed $var variable to be dumped
* @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10.
* @param boolean $highlight whether the result should be syntax-highlighted
* @return string the string representation of the variable
*/
public static function dumpAsString($var, $depth = 10, $highlight = false)
{
self::$_output = '';
self::$_objects = array();
self::$_depth = $depth;
self::dumpInternal($var, 0);
if ($highlight) {
$result = highlight_string("<?php\n" . self::$_output, true);
self::$_output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
}
return self::$_output;
}
/**
* @param mixed $var variable to be dumped
* @param integer $level depth level
*/
private static function dumpInternal($var, $level)
{
switch (gettype($var)) {
case 'boolean':
self::$_output .= $var ? 'true' : 'false';
break;
case 'integer':
self::$_output .= "$var";
break;
case 'double':
self::$_output .= "$var";
break;
case 'string':
self::$_output .= "'" . addslashes($var) . "'";
break;
case 'resource':
self::$_output .= '{resource}';
break;
case 'NULL':
self::$_output .= "null";
break;
case 'unknown type':
self::$_output .= '{unknown}';
break;
case 'array':
if (self::$_depth <= $level) {
self::$_output .= 'array(...)';
} elseif (empty($var)) {
self::$_output .= 'array()';
} else {
$keys = array_keys($var);
$spaces = str_repeat(' ', $level * 4);
self::$_output .= "array\n" . $spaces . '(';
foreach ($keys as $key) {
self::$_output .= "\n" . $spaces . ' ';
self::dumpInternal($key, 0);
self::$_output .= ' => ';
self::dumpInternal($var[$key], $level + 1);
}
self::$_output .= "\n" . $spaces . ')';
}
break;
case 'object':
if (($id = array_search($var, self::$_objects, true)) !== false) {
self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)';
} elseif (self::$_depth <= $level) {
self::$_output .= get_class($var) . '(...)';
} else {
$id = self::$_objects[] = $var;
$className = get_class($var);
$members = (array)$var;
$spaces = str_repeat(' ', $level * 4);
self::$_output .= "$className#$id\n" . $spaces . '(';
foreach ($members as $key => $value) {
$keyDisplay = strtr(trim($key), array("\0" => ':'));
self::$_output .= "\n" . $spaces . " [$keyDisplay] => ";
self::dumpInternal($value, $level + 1);
}
self::$_output .= "\n" . $spaces . ')';
}
break;
}
}
}

0
framework/helpers/mimeTypes.php → framework/helpers/base/mimeTypes.php

97
framework/i18n/I18N.php

@ -1,11 +1,23 @@
<?php <?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\i18n; namespace yii\i18n;
use Yii; use Yii;
use yii\base\Component; use yii\base\Component;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
/**
* I18N provides features related with internationalization (I18N) and localization (L10N).
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class I18N extends Component class I18N extends Component
{ {
/** /**
@ -13,11 +25,36 @@ class I18N extends Component
* categories, and the array values are the corresponding [[MessageSource]] objects or the configurations * categories, and the array values are the corresponding [[MessageSource]] objects or the configurations
* for creating the [[MessageSource]] objects. The message categories can contain the wildcard '*' at the end * for creating the [[MessageSource]] objects. The message categories can contain the wildcard '*' at the end
* to match multiple categories with the same prefix. For example, 'app\*' matches both 'app\cat1' and 'app\cat2'. * to match multiple categories with the same prefix. For example, 'app\*' matches both 'app\cat1' and 'app\cat2'.
*
* This property may be modified on the fly by extensions who want to have their own message sources
* registered under their own namespaces.
*
* The category "yii" and "app" are always defined. The former refers to the messages used in the Yii core
* framework code, while the latter refers to the default message category for custom application code.
* By default, both of these categories use [[PhpMessageSource]] and the corresponding message files are
* stored under "@yii/messages" and "@app/messages", respectively.
*
* You may override the configuration of both categories.
*/ */
public $translations; public $translations;
/**
* @var string the path or path alias of the file that contains the plural rules.
* By default, this refers to a file shipped with the Yii distribution. The file is obtained
* by converting from the data file in the CLDR project.
*
* If the default rule file does not contain the expected rules, you may copy and modify it
* for your application, and then configure this property to point to your modified copy.
*
* @see http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html
*/
public $pluralRuleFile = '@yii/i18n/data/plurals.php';
/**
* Initializes the component by configuring the default message categories.
*/
public function init() public function init()
{ {
parent::init();
if (!isset($this->translations['yii'])) { if (!isset($this->translations['yii'])) {
$this->translations['yii'] = array( $this->translations['yii'] = array(
'class' => 'yii\i18n\PhpMessageSource', 'class' => 'yii\i18n\PhpMessageSource',
@ -34,6 +71,16 @@ class I18N extends Component
} }
} }
/**
* Translates a message to the specified language.
* If the first parameter in `$params` is a number and it is indexed by 0, appropriate plural rules
* will be applied to the translated message.
* @param string $message the message to be translated.
* @param array $params the parameters that will be used to replace the corresponding placeholders in the message.
* @param string $language the language code (e.g. `en_US`, `en`). If this is null, the current
* [[\yii\base\Application::language|application language]] will be used.
* @return string the translated message.
*/
public function translate($message, $params = array(), $language = null) public function translate($message, $params = array(), $language = null)
{ {
if ($language === null) { if ($language === null) {
@ -55,7 +102,7 @@ class I18N extends Component
} }
if (isset($params[0])) { if (isset($params[0])) {
$message = $this->getPluralForm($message, $params[0], $language); $message = $this->applyPluralRules($message, $params[0], $language);
if (!isset($params['{n}'])) { if (!isset($params['{n}'])) {
$params['{n}'] = $params[0]; $params['{n}'] = $params[0];
} }
@ -65,6 +112,12 @@ class I18N extends Component
return $params === array() ? $message : strtr($message, $params); return $params === array() ? $message : strtr($message, $params);
} }
/**
* Returns the message source for the given category.
* @param string $category the category name.
* @return MessageSource the message source for the given category.
* @throws InvalidConfigException if there is no message source available for the specified category.
*/
public function getMessageSource($category) public function getMessageSource($category)
{ {
if (isset($this->translations[$category])) { if (isset($this->translations[$category])) {
@ -85,18 +138,21 @@ class I18N extends Component
} }
} }
public function getLocale($language) /**
{ * Applies appropriate plural rules to the given message.
* @param string $message the message to be applied with plural rules
} * @param mixed $number the number by which plural rules will be applied
* @param string $language the language code that determines which set of plural rules to be applied.
protected function getPluralForm($message, $number, $language) * @return string the message that has applied plural rules
*/
protected function applyPluralRules($message, $number, $language)
{ {
if (strpos($message, '|') === false) { if (strpos($message, '|') === false) {
return $message; return $message;
} }
$chunks = explode('|', $message); $chunks = explode('|', $message);
$rules = $this->getLocale($language)->getPluralRules();
$rules = $this->getPluralRules($language);
foreach ($rules as $i => $rule) { foreach ($rules as $i => $rule) {
if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { if (isset($chunks[$i]) && $this->evaluate($rule, $number)) {
return $chunks[$i]; return $chunks[$i];
@ -106,6 +162,29 @@ class I18N extends Component
return isset($chunks[$n]) ? $chunks[$n] : $chunks[0]; return isset($chunks[$n]) ? $chunks[$n] : $chunks[0];
} }
private $_pluralRules = array(); // language => rule set
/**
* Returns the plural rules for the given language code.
* @param string $language the language code (e.g. `en_US`, `en`).
* @return array the plural rules
* @throws InvalidParamException if the language code is invalid.
*/
protected function getPluralRules($language)
{
if (isset($this->_pluralRules[$language])) {
return $this->_pluralRules;
}
$allRules = require(Yii::getAlias($this->pluralRuleFile));
if (isset($allRules[$language])) {
return $this->_pluralRules[$language] = $allRules[$language];
} elseif (preg_match('/^[a-z]+/', strtolower($language), $matches)) {
return $this->_pluralRules[$language] = isset($allRules[$matches[0]]) ? $allRules[$matches[0]] : array();
} else {
throw new InvalidParamException("Invalid language code: $language");
}
}
/** /**
* Evaluates a PHP expression with the given number value. * Evaluates a PHP expression with the given number value.
* @param string $expression the PHP expression * @param string $expression the PHP expression
@ -114,6 +193,6 @@ class I18N extends Component
*/ */
protected function evaluate($expression, $n) protected function evaluate($expression, $n)
{ {
return @eval("return $expression;"); return eval("return $expression;");
} }
} }

2
framework/i18n/PhpMessageSource.php

@ -72,7 +72,7 @@ class PhpMessageSource extends MessageSource
} }
return $messages; return $messages;
} else { } else {
Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__);
return array(); return array();
} }
} }

627
framework/i18n/data/plurals.php

@ -0,0 +1,627 @@
<?php
/**
* Plural rules.
*
* This file is automatically generated by the "yiic locale/plural" command under the "build" folder.
* Do not modify it directly.
*
* The original plural rule data used for generating this file has the following copyright terms:
*
* Copyright © 1991-2007 Unicode, Inc. All rights reserved.
* Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
*
* @revision 6008 (of the original plural file)
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
return array (
'ar' =>
array (
0 => '$n==0',
1 => '$n==1',
2 => '$n==2',
3 => 'in_array(fmod($n,100),range(3,10))',
4 => 'in_array(fmod($n,100),range(11,99))',
),
'asa' =>
array (
0 => '$n==1',
),
'af' =>
array (
0 => '$n==1',
),
'bem' =>
array (
0 => '$n==1',
),
'bez' =>
array (
0 => '$n==1',
),
'bg' =>
array (
0 => '$n==1',
),
'bn' =>
array (
0 => '$n==1',
),
'brx' =>
array (
0 => '$n==1',
),
'ca' =>
array (
0 => '$n==1',
),
'cgg' =>
array (
0 => '$n==1',
),
'chr' =>
array (
0 => '$n==1',
),
'da' =>
array (
0 => '$n==1',
),
'de' =>
array (
0 => '$n==1',
),
'dv' =>
array (
0 => '$n==1',
),
'ee' =>
array (
0 => '$n==1',
),
'el' =>
array (
0 => '$n==1',
),
'en' =>
array (
0 => '$n==1',
),
'eo' =>
array (
0 => '$n==1',
),
'es' =>
array (
0 => '$n==1',
),
'et' =>
array (
0 => '$n==1',
),
'eu' =>
array (
0 => '$n==1',
),
'fi' =>
array (
0 => '$n==1',
),
'fo' =>
array (
0 => '$n==1',
),
'fur' =>
array (
0 => '$n==1',
),
'fy' =>
array (
0 => '$n==1',
),
'gl' =>
array (
0 => '$n==1',
),
'gsw' =>
array (
0 => '$n==1',
),
'gu' =>
array (
0 => '$n==1',
),
'ha' =>
array (
0 => '$n==1',
),
'haw' =>
array (
0 => '$n==1',
),
'he' =>
array (
0 => '$n==1',
),
'is' =>
array (
0 => '$n==1',
),
'it' =>
array (
0 => '$n==1',
),
'jmc' =>
array (
0 => '$n==1',
),
'kaj' =>
array (
0 => '$n==1',
),
'kcg' =>
array (
0 => '$n==1',
),
'kk' =>
array (
0 => '$n==1',
),
'kl' =>
array (
0 => '$n==1',
),
'ksb' =>
array (
0 => '$n==1',
),
'ku' =>
array (
0 => '$n==1',
),
'lb' =>
array (
0 => '$n==1',
),
'lg' =>
array (
0 => '$n==1',
),
'mas' =>
array (
0 => '$n==1',
),
'ml' =>
array (
0 => '$n==1',
),
'mn' =>
array (
0 => '$n==1',
),
'mr' =>
array (
0 => '$n==1',
),
'nah' =>
array (
0 => '$n==1',
),
'nb' =>
array (
0 => '$n==1',
),
'nd' =>
array (
0 => '$n==1',
),
'ne' =>
array (
0 => '$n==1',
),
'nl' =>
array (
0 => '$n==1',
),
'nn' =>
array (
0 => '$n==1',
),
'no' =>
array (
0 => '$n==1',
),
'nr' =>
array (
0 => '$n==1',
),
'ny' =>
array (
0 => '$n==1',
),
'nyn' =>
array (
0 => '$n==1',
),
'om' =>
array (
0 => '$n==1',
),
'or' =>
array (
0 => '$n==1',
),
'pa' =>
array (
0 => '$n==1',
),
'pap' =>
array (
0 => '$n==1',
),
'ps' =>
array (
0 => '$n==1',
),
'pt' =>
array (
0 => '$n==1',
),
'rof' =>
array (
0 => '$n==1',
),
'rm' =>
array (
0 => '$n==1',
),
'rwk' =>
array (
0 => '$n==1',
),
'saq' =>
array (
0 => '$n==1',
),
'seh' =>
array (
0 => '$n==1',
),
'sn' =>
array (
0 => '$n==1',
),
'so' =>
array (
0 => '$n==1',
),
'sq' =>
array (
0 => '$n==1',
),
'ss' =>
array (
0 => '$n==1',
),
'ssy' =>
array (
0 => '$n==1',
),
'st' =>
array (
0 => '$n==1',
),
'sv' =>
array (
0 => '$n==1',
),
'sw' =>
array (
0 => '$n==1',
),
'syr' =>
array (
0 => '$n==1',
),
'ta' =>
array (
0 => '$n==1',
),
'te' =>
array (
0 => '$n==1',
),
'teo' =>
array (
0 => '$n==1',
),
'tig' =>
array (
0 => '$n==1',
),
'tk' =>
array (
0 => '$n==1',
),
'tn' =>
array (
0 => '$n==1',
),
'ts' =>
array (
0 => '$n==1',
),
'ur' =>
array (
0 => '$n==1',
),
'wae' =>
array (
0 => '$n==1',
),
've' =>
array (
0 => '$n==1',
),
'vun' =>
array (
0 => '$n==1',
),
'xh' =>
array (
0 => '$n==1',
),
'xog' =>
array (
0 => '$n==1',
),
'zu' =>
array (
0 => '$n==1',
),
'ak' =>
array (
0 => '($n==0||$n==1)',
),
'am' =>
array (
0 => '($n==0||$n==1)',
),
'bh' =>
array (
0 => '($n==0||$n==1)',
),
'fil' =>
array (
0 => '($n==0||$n==1)',
),
'tl' =>
array (
0 => '($n==0||$n==1)',
),
'guw' =>
array (
0 => '($n==0||$n==1)',
),
'hi' =>
array (
0 => '($n==0||$n==1)',
),
'ln' =>
array (
0 => '($n==0||$n==1)',
),
'mg' =>
array (
0 => '($n==0||$n==1)',
),
'nso' =>
array (
0 => '($n==0||$n==1)',
),
'ti' =>
array (
0 => '($n==0||$n==1)',
),
'wa' =>
array (
0 => '($n==0||$n==1)',
),
'ff' =>
array (
0 => '($n>=0&&$n<=2)&&$n!=2',
),
'fr' =>
array (
0 => '($n>=0&&$n<=2)&&$n!=2',
),
'kab' =>
array (
0 => '($n>=0&&$n<=2)&&$n!=2',
),
'lv' =>
array (
0 => '$n==0',
1 => 'fmod($n,10)==1&&fmod($n,100)!=11',
),
'iu' =>
array (
0 => '$n==1',
1 => '$n==2',
),
'kw' =>
array (
0 => '$n==1',
1 => '$n==2',
),
'naq' =>
array (
0 => '$n==1',
1 => '$n==2',
),
'se' =>
array (
0 => '$n==1',
1 => '$n==2',
),
'sma' =>
array (
0 => '$n==1',
1 => '$n==2',
),
'smi' =>
array (
0 => '$n==1',
1 => '$n==2',
),
'smj' =>
array (
0 => '$n==1',
1 => '$n==2',
),
'smn' =>
array (
0 => '$n==1',
1 => '$n==2',
),
'sms' =>
array (
0 => '$n==1',
1 => '$n==2',
),
'ga' =>
array (
0 => '$n==1',
1 => '$n==2',
2 => 'in_array($n,array(3,4,5,6))',
3 => 'in_array($n,array(7,8,9,10))',
),
'ro' =>
array (
0 => '$n==1',
1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))',
),
'mo' =>
array (
0 => '$n==1',
1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))',
),
'lt' =>
array (
0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),range(11,19))',
1 => 'in_array(fmod($n,10),range(2,9))&&!in_array(fmod($n,100),range(11,19))',
),
'be' =>
array (
0 => 'fmod($n,10)==1&&fmod($n,100)!=11',
1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))',
2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))',
),
'bs' =>
array (
0 => 'fmod($n,10)==1&&fmod($n,100)!=11',
1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))',
2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))',
),
'hr' =>
array (
0 => 'fmod($n,10)==1&&fmod($n,100)!=11',
1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))',
2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))',
),
'ru' =>
array (
0 => 'fmod($n,10)==1&&fmod($n,100)!=11',
1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))',
2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))',
),
'sh' =>
array (
0 => 'fmod($n,10)==1&&fmod($n,100)!=11',
1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))',
2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))',
),
'sr' =>
array (
0 => 'fmod($n,10)==1&&fmod($n,100)!=11',
1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))',
2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))',
),
'uk' =>
array (
0 => 'fmod($n,10)==1&&fmod($n,100)!=11',
1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))',
2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))',
),
'cs' =>
array (
0 => '$n==1',
1 => 'in_array($n,array(2,3,4))',
),
'sk' =>
array (
0 => '$n==1',
1 => 'in_array($n,array(2,3,4))',
),
'pl' =>
array (
0 => '$n==1',
1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))',
2 => '$n!=1&&in_array(fmod($n,10),array(0,1))||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(12,13,14))',
),
'sl' =>
array (
0 => 'fmod($n,100)==1',
1 => 'fmod($n,100)==2',
2 => 'in_array(fmod($n,100),array(3,4))',
),
'mt' =>
array (
0 => '$n==1',
1 => '$n==0||in_array(fmod($n,100),range(2,10))',
2 => 'in_array(fmod($n,100),range(11,19))',
),
'mk' =>
array (
0 => 'fmod($n,10)==1&&$n!=11',
),
'cy' =>
array (
0 => '$n==0',
1 => '$n==1',
2 => '$n==2',
3 => '$n==3',
4 => '$n==6',
),
'lag' =>
array (
0 => '$n==0',
1 => '($n>=0&&$n<=2)&&$n!=0&&$n!=2',
),
'shi' =>
array (
0 => '($n>=0&&$n<=1)',
1 => 'in_array($n,range(2,10))',
),
'br' =>
array (
0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))',
1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))',
2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))',
3 => 'fmod($n,1000000)==0&&$n!=0',
),
'ksh' =>
array (
0 => '$n==0',
1 => '$n==1',
),
'tzm' =>
array (
0 => '($n==0||$n==1)||in_array($n,range(11,99))',
),
'gv' =>
array (
0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0',
),
);

109
framework/i18n/data/plurals.xml

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd">
<supplementalData>
<version number="$Revision: 6008 $"/>
<generation date="$Date: 2011-07-12 13:18:01 -0500 (Tue, 12 Jul 2011) $"/>
<plurals>
<!-- if locale is known to have no plurals, there are no rules -->
<pluralRules locales="az bm bo dz fa id ig ii hu ja jv ka kde kea km kn ko lo ms my sah ses sg th to tr vi wo yo zh"/>
<pluralRules locales="ar">
<pluralRule count="zero">n is 0</pluralRule>
<pluralRule count="one">n is 1</pluralRule>
<pluralRule count="two">n is 2</pluralRule>
<pluralRule count="few">n mod 100 in 3..10</pluralRule>
<pluralRule count="many">n mod 100 in 11..99</pluralRule>
</pluralRules>
<pluralRules locales="asa af bem bez bg bn brx ca cgg chr da de dv ee el en eo es et eu fi fo fur fy gl gsw gu ha haw he is it jmc kaj kcg kk kl ksb ku lb lg mas ml mn mr nah nb nd ne nl nn no nr ny nyn om or pa pap ps pt rof rm rwk saq seh sn so sq ss ssy st sv sw syr ta te teo tig tk tn ts ur wae ve vun xh xog zu">
<pluralRule count="one">n is 1</pluralRule>
</pluralRules>
<pluralRules locales="ak am bh fil tl guw hi ln mg nso ti wa">
<pluralRule count="one">n in 0..1</pluralRule>
</pluralRules>
<pluralRules locales="ff fr kab">
<pluralRule count="one">n within 0..2 and n is not 2</pluralRule>
</pluralRules>
<pluralRules locales="lv">
<pluralRule count="zero">n is 0</pluralRule>
<pluralRule count="one">n mod 10 is 1 and n mod 100 is not 11</pluralRule>
</pluralRules>
<pluralRules locales="iu kw naq se sma smi smj smn sms">
<pluralRule count="one">n is 1</pluralRule>
<pluralRule count="two">n is 2</pluralRule>
</pluralRules>
<pluralRules locales="ga"> <!-- http://unicode.org/cldr/trac/ticket/3915 -->
<pluralRule count="one">n is 1</pluralRule>
<pluralRule count="two">n is 2</pluralRule>
<pluralRule count="few">n in 3..6</pluralRule>
<pluralRule count="many">n in 7..10</pluralRule>
</pluralRules>
<pluralRules locales="ro mo">
<pluralRule count="one">n is 1</pluralRule>
<pluralRule count="few">n is 0 OR n is not 1 AND n mod 100 in 1..19</pluralRule>
</pluralRules>
<pluralRules locales="lt">
<pluralRule count="one">n mod 10 is 1 and n mod 100 not in 11..19</pluralRule>
<pluralRule count="few">n mod 10 in 2..9 and n mod 100 not in 11..19</pluralRule>
</pluralRules>
<pluralRules locales="be bs hr ru sh sr uk">
<pluralRule count="one">n mod 10 is 1 and n mod 100 is not 11</pluralRule>
<pluralRule count="few">n mod 10 in 2..4 and n mod 100 not in 12..14</pluralRule>
<pluralRule count="many">n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14</pluralRule>
<!-- others are fractions -->
</pluralRules>
<pluralRules locales="cs sk">
<pluralRule count="one">n is 1</pluralRule>
<pluralRule count="few">n in 2..4</pluralRule>
</pluralRules>
<pluralRules locales="pl">
<pluralRule count="one">n is 1</pluralRule>
<pluralRule count="few">n mod 10 in 2..4 and n mod 100 not in 12..14</pluralRule>
<pluralRule count="many">n is not 1 and n mod 10 in 0..1 or n mod 10 in 5..9 or n mod 100 in 12..14</pluralRule>
<!-- others are fractions -->
<!-- and n mod 100 not in 22..24 from Tamplin -->
</pluralRules>
<pluralRules locales="sl">
<pluralRule count="one">n mod 100 is 1</pluralRule>
<pluralRule count="two">n mod 100 is 2</pluralRule>
<pluralRule count="few">n mod 100 in 3..4</pluralRule>
</pluralRules>
<pluralRules locales="mt"> <!-- from Tamplin's data -->
<pluralRule count="one">n is 1</pluralRule>
<pluralRule count="few">n is 0 or n mod 100 in 2..10</pluralRule>
<pluralRule count="many">n mod 100 in 11..19</pluralRule>
</pluralRules>
<pluralRules locales="mk"> <!-- from Tamplin's data -->
<pluralRule count="one">n mod 10 is 1 and n is not 11</pluralRule>
</pluralRules>
<pluralRules locales="cy"> <!-- from http://www.saltcymru.org/wordpress/?p=99&lang=en -->
<pluralRule count="zero">n is 0</pluralRule>
<pluralRule count="one">n is 1</pluralRule>
<pluralRule count="two">n is 2</pluralRule>
<pluralRule count="few">n is 3</pluralRule>
<pluralRule count="many">n is 6</pluralRule>
</pluralRules>
<pluralRules locales="lag">
<pluralRule count="zero">n is 0</pluralRule>
<pluralRule count="one">n within 0..2 and n is not 0 and n is not 2</pluralRule>
</pluralRules>
<pluralRules locales="shi">
<pluralRule count="one">n within 0..1</pluralRule>
<pluralRule count="few">n in 2..10</pluralRule>
</pluralRules>
<pluralRules locales="br"> <!-- from http://unicode.org/cldr/trac/ticket/2886 -->
<pluralRule count="one">n mod 10 is 1 and n mod 100 not in 11,71,91</pluralRule>
<pluralRule count="two">n mod 10 is 2 and n mod 100 not in 12,72,92</pluralRule>
<pluralRule count="few">n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99</pluralRule>
<pluralRule count="many">n mod 1000000 is 0 and n is not 0</pluralRule>
</pluralRules>
<pluralRules locales="ksh">
<pluralRule count="zero">n is 0</pluralRule>
<pluralRule count="one">n is 1</pluralRule>
</pluralRules>
<pluralRules locales="tzm">
<pluralRule count="one">n in 0..1 or n in 11..99</pluralRule>
</pluralRules>
<pluralRules locales="gv">
<pluralRule count="one">n mod 10 in 1..2 or n mod 20 is 0</pluralRule>
</pluralRules>
</plurals>
</supplementalData>

3
framework/logging/DbTarget.php

@ -78,7 +78,8 @@ class DbTarget extends Target
public function export($messages) public function export($messages)
{ {
$tableName = $this->db->quoteTableName($this->logTable); $tableName = $this->db->quoteTableName($this->logTable);
$sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; $sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[message]])
VALUES (:level, :category, :log_time, :message)";
$command = $this->db->createCommand($sql); $command = $this->db->createCommand($sql);
foreach ($messages as $message) { foreach ($messages as $message) {
$command->bindValues(array( $command->bindValues(array(

24
framework/logging/FileTarget.php

@ -6,6 +6,8 @@
*/ */
namespace yii\logging; namespace yii\logging;
use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
/** /**
@ -23,15 +25,14 @@ use yii\base\InvalidConfigException;
class FileTarget extends Target class FileTarget extends Target
{ {
/** /**
* @var string log file path or path alias. If not set, it means the 'application.log' file under * @var string log file path or path alias. If not set, it will use the "runtime/logs/app.log" file.
* the application runtime directory. Please make sure the directory containing * The directory containing the log files will be automatically created if not existing.
* the log file is writable by the Web server process.
*/ */
public $logFile; public $logFile;
/** /**
* @var integer maximum log file size, in kilo-bytes. Defaults to 1024, meaning 1MB. * @var integer maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB.
*/ */
public $maxFileSize = 1024; // in KB public $maxFileSize = 10240; // in KB
/** /**
* @var integer number of log files used for rotation. Defaults to 5. * @var integer number of log files used for rotation. Defaults to 5.
*/ */
@ -46,13 +47,13 @@ class FileTarget extends Target
{ {
parent::init(); parent::init();
if ($this->logFile === null) { if ($this->logFile === null) {
$this->logFile = \Yii::$app->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log';
} else { } else {
$this->logFile = \Yii::getAlias($this->logFile); $this->logFile = Yii::getAlias($this->logFile);
} }
$logPath = dirname($this->logFile); $logPath = dirname($this->logFile);
if (!is_dir($logPath) || !is_writable($logPath)) { if (!is_dir($logPath)) {
throw new InvalidConfigException("Directory '$logPath' does not exist or is not writable."); @mkdir($logPath, 0777, true);
} }
if ($this->maxLogFiles < 1) { if ($this->maxLogFiles < 1) {
$this->maxLogFiles = 1; $this->maxLogFiles = 1;
@ -66,6 +67,7 @@ class FileTarget extends Target
* Sends log messages to specified email addresses. * Sends log messages to specified email addresses.
* @param array $messages the messages to be exported. See [[Logger::messages]] for the structure * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure
* of each message. * of each message.
* @throws InvalidConfigException if unable to open the log file for writing
*/ */
public function export($messages) public function export($messages)
{ {
@ -73,7 +75,9 @@ class FileTarget extends Target
foreach ($messages as $message) { foreach ($messages as $message) {
$text .= $this->formatMessage($message); $text .= $this->formatMessage($message);
} }
$fp = @fopen($this->logFile, 'a'); if (($fp = @fopen($this->logFile, 'a')) === false) {
throw new InvalidConfigException("Unable to append to log file: {$this->logFile}");
}
@flock($fp, LOCK_EX); @flock($fp, LOCK_EX);
if (@filesize($this->logFile) > $this->maxFileSize * 1024) { if (@filesize($this->logFile) > $this->maxFileSize * 1024) {
$this->rotateFiles(); $this->rotateFiles();

3
framework/logging/Target.php

@ -238,6 +238,7 @@ abstract class Target extends \yii\base\Component
if (!is_string($text)) { if (!is_string($text)) {
$text = var_export($text, true); $text = var_export($text, true);
} }
return date('Y/m/d H:i:s', $timestamp) . " [$level] [$category] $text\n"; $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1';
return date('Y/m/d H:i:s', $timestamp) . " [$ip] [$level] [$category] $text\n";
} }
} }

39
framework/validators/BooleanValidator.php

@ -7,6 +7,8 @@
namespace yii\validators; namespace yii\validators;
use Yii;
/** /**
* BooleanValidator checks if the attribute value is a boolean value. * BooleanValidator checks if the attribute value is a boolean value.
* *
@ -32,11 +34,17 @@ class BooleanValidator extends Validator
* Defaults to false, meaning only the value needs to be matched. * Defaults to false, meaning only the value needs to be matched.
*/ */
public $strict = false; public $strict = false;
/** /**
* @var boolean whether the attribute value can be null or empty. Defaults to true, * Initializes the validator.
* meaning that if the attribute is empty, it is considered valid.
*/ */
public $allowEmpty = true; public function init()
{
parent::init();
if ($this->message === null) {
$this->message = Yii::t('yii|{attribute} must be either "{true}" or "{false}".');
}
}
/** /**
* Validates the attribute of the object. * Validates the attribute of the object.
@ -47,13 +55,8 @@ class BooleanValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) { if (!$this->validateValue($value)) {
return; $this->addError($object, $attribute, $this->message, array(
}
if (!$this->strict && $value != $this->trueValue && $value != $this->falseValue
|| $this->strict && $value !== $this->trueValue && $value !== $this->falseValue) {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.');
$this->addError($object, $attribute, $message, array(
'{true}' => $this->trueValue, '{true}' => $this->trueValue,
'{false}' => $this->falseValue, '{false}' => $this->falseValue,
)); ));
@ -61,6 +64,17 @@ class BooleanValidator extends Validator
} }
/** /**
* Validates the given value.
* @param mixed $value the value to be validated.
* @return boolean whether the value is valid.
*/
public function validateValue($value)
{
return $this->strict && ($value == $this->trueValue || $value == $this->falseValue)
|| !$this->strict && ($value === $this->trueValue || $value === $this->falseValue);
}
/**
* Returns the JavaScript needed for performing client-side validation. * Returns the JavaScript needed for performing client-side validation.
* @param \yii\base\Model $object the data object being validated * @param \yii\base\Model $object the data object being validated
* @param string $attribute the name of the attribute to be validated. * @param string $attribute the name of the attribute to be validated.
@ -68,15 +82,14 @@ class BooleanValidator extends Validator
*/ */
public function clientValidateAttribute($object, $attribute) public function clientValidateAttribute($object, $attribute)
{ {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); $message = strtr($this->message, array(
$message = strtr($message, array(
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{value}' => $object->$attribute, '{value}' => $object->$attribute,
'{true}' => $this->trueValue, '{true}' => $this->trueValue,
'{false}' => $this->falseValue, '{false}' => $this->falseValue,
)); ));
return " return "
if(" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . "value!=" . json_encode($this->trueValue) . " && value!=" . json_encode($this->falseValue) . ") { if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . "value!=" . json_encode($this->trueValue) . " && value!=" . json_encode($this->falseValue) . ") {
messages.push(" . json_encode($message) . "); messages.push(" . json_encode($message) . ");
} }
"; ";

64
framework/validators/CaptchaValidator.php

@ -7,6 +7,9 @@
namespace yii\validators; namespace yii\validators;
use Yii;
use yii\base\InvalidConfigException;
/** /**
* CaptchaValidator validates that the attribute value is the same as the verification code displayed in the CAPTCHA. * CaptchaValidator validates that the attribute value is the same as the verification code displayed in the CAPTCHA.
* *
@ -22,16 +25,21 @@ class CaptchaValidator extends Validator
*/ */
public $caseSensitive = false; public $caseSensitive = false;
/** /**
* @var string the ID of the action that renders the CAPTCHA image. Defaults to 'captcha', * @var string the route of the controller action that renders the CAPTCHA image.
* meaning the `captcha` action declared in the current controller.
* This can also be a route consisting of controller ID and action ID (e.g. 'site/captcha').
*/ */
public $captchaAction = 'captcha'; public $captchaAction = 'site/captcha';
/** /**
* @var boolean whether the attribute value can be null or empty. * Initializes the validator.
* Defaults to false, meaning the attribute is invalid if it is empty.
*/ */
public $allowEmpty = false; public function init()
{
parent::init();
if ($this->message === null) {
$this->message = Yii::t('yii|The verification code is incorrect.');
}
}
/** /**
* Validates the attribute of the object. * Validates the attribute of the object.
@ -42,36 +50,39 @@ class CaptchaValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) { if (!$this->validateValue($value)) {
return; $this->addError($object, $attribute, $this->message);
} }
$captcha = $this->getCaptchaAction();
if (!$captcha->validate($value, $this->caseSensitive)) {
$message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.');
$this->addError($object, $attribute, $message);
} }
/**
* Validates the given value.
* @param mixed $value the value to be validated.
* @return boolean whether the value is valid.
*/
public function validateValue($value)
{
$captcha = $this->getCaptchaAction();
return !is_array($value) && $captcha->validate($value, $this->caseSensitive);
} }
/** /**
* Returns the CAPTCHA action object. * Returns the CAPTCHA action object.
* @return CCaptchaAction the action object * @throws InvalidConfigException
* @return CaptchaAction the action object
*/ */
public function getCaptchaAction() public function getCaptchaAction()
{ {
if (strpos($this->captchaAction, '/') !== false) { // contains controller or module $ca = Yii::$app->createController($this->captchaAction);
$ca = \Yii::$app->createController($this->captchaAction); if ($ca !== false) {
if ($ca !== null) { /** @var \yii\base\Controller $controller */
list($controller, $actionID) = $ca; list($controller, $actionID) = $ca;
$action = $controller->createAction($actionID); $action = $controller->createAction($actionID);
if ($action !== null) {
return $action;
} }
} else {
$action = \Yii::$app->getController()->createAction($this->captchaAction);
}
if ($action === null) {
throw new \yii\base\Exception('Invalid captcha action ID: ' . $this->captchaAction);
} }
return $action; throw new InvalidConfigException('Invalid CAPTCHA action ID: ' . $this->captchaAction);
} }
/** /**
@ -83,8 +94,7 @@ class CaptchaValidator extends Validator
public function clientValidateAttribute($object, $attribute) public function clientValidateAttribute($object, $attribute)
{ {
$captcha = $this->getCaptchaAction(); $captcha = $this->getCaptchaAction();
$message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); $message = strtr($this->message, array(
$message = strtr($message, array(
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{value}' => $object->$attribute, '{value}' => $object->$attribute,
)); ));
@ -102,7 +112,7 @@ if(h != hash) {
} }
"; ";
if ($this->allowEmpty) { if ($this->skipOnEmpty) {
$js = " $js = "
if($.trim(value)!='') { if($.trim(value)!='') {
$js $js

185
framework/validators/CompareValidator.php

@ -6,6 +6,7 @@
*/ */
namespace yii\validators; namespace yii\validators;
use Yii; use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
@ -45,23 +46,12 @@ class CompareValidator extends Validator
*/ */
public $compareValue; public $compareValue;
/** /**
* @var boolean whether the comparison is strict (both value and type must be the same.) * @var string the operator for comparison. The following operators are supported:
* Defaults to false.
*/
public $strict = false;
/**
* @var boolean whether the attribute value can be null or empty. Defaults to false.
* If this is true, it means the attribute is considered valid when it is empty.
*/
public $allowEmpty = false;
/**
* @var string the operator for comparison. Defaults to '='.
* The followings are valid operators:
* *
* - `=` or `==`: validates to see if the two values are equal. If [[strict]] is true, the comparison * - '==': validates to see if the two values are equal. The comparison is done is non-strict mode.
* will be done in strict mode (i.e. checking value type as well). * - '===': validates to see if the two values are equal. The comparison is done is strict mode.
* - `!=`: validates to see if the two values are NOT equal. If [[strict]] is true, the comparison * - '!=': validates to see if the two values are NOT equal. The comparison is done is non-strict mode.
* will be done in strict mode (i.e. checking value type as well). * - '!==': validates to see if the two values are NOT equal. The comparison is done is strict mode.
* - `>`: validates to see if the value being validated is greater than the value being compared with. * - `>`: validates to see if the value being validated is greater than the value being compared with.
* - `>=`: validates to see if the value being validated is greater than or equal to the value being compared with. * - `>=`: validates to see if the value being validated is greater than or equal to the value being compared with.
* - `<`: validates to see if the value being validated is less than the value being compared with. * - `<`: validates to see if the value being validated is less than the value being compared with.
@ -69,6 +59,45 @@ class CompareValidator extends Validator
*/ */
public $operator = '='; public $operator = '=';
/**
* Initializes the validator.
*/
public function init()
{
parent::init();
if ($this->message === null) {
switch ($this->operator) {
case '==':
$this->message = Yii::t('yii|{attribute} must be repeated exactly.');
break;
case '===':
$this->message = Yii::t('yii|{attribute} must be repeated exactly.');
break;
case '!=':
$this->message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".');
break;
case '!==':
$this->message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".');
break;
case '>':
$this->message = Yii::t('yii|{attribute} must be greater than "{compareValue}".');
break;
case '>=':
$this->message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".');
break;
case '<':
$this->message = Yii::t('yii|{attribute} must be less than "{compareValue}".');
break;
case '<=':
$this->message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".');
break;
default:
throw new InvalidConfigException("Unknown operator: {$this->operator}");
}
}
}
/** /**
* Validates the attribute of the object. * Validates the attribute of the object.
* If there is any error, the error message is added to the object. * If there is any error, the error message is added to the object.
@ -79,7 +108,8 @@ class CompareValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) { if (is_array($value)) {
$this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.'));
return; return;
} }
if ($this->compareValue !== null) { if ($this->compareValue !== null) {
@ -91,45 +121,45 @@ class CompareValidator extends Validator
} }
switch ($this->operator) { switch ($this->operator) {
case '=': case '==': $valid = $value == $compareValue; break;
case '==': case '===': $valid = $value === $compareValue; break;
if (($this->strict && $value !== $compareValue) || (!$this->strict && $value != $compareValue)) { case '!=': $valid = $value != $compareValue; break;
$message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); case '!==': $valid = $value !== $compareValue; break;
$this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); case '>': $valid = $value > $compareValue; break;
} case '>=': $valid = $value >= $compareValue; break;
break; case '<': $valid = $value < $compareValue; break;
case '!=': case '<=': $valid = $value <= $compareValue; break;
if (($this->strict && $value === $compareValue) || (!$this->strict && $value == $compareValue)) { default: $valid = false; break;
$message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); }
$this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); if (!$valid) {
} $this->addError($object, $attribute, $this->message, array(
break; '{compareAttribute}' => $compareLabel,
case '>': '{compareValue}' => $compareValue,
if ($value <= $compareValue) { ));
$message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than "{compareValue}".');
$this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue));
}
break;
case '>=':
if ($value < $compareValue) {
$message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".');
$this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue));
} }
break;
case '<':
if ($value >= $compareValue) {
$message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than "{compareValue}".');
$this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue));
} }
break;
case '<=': /**
if ($value > $compareValue) { * Validates the given value.
$message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); * @param mixed $value the value to be validated.
$this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); * @return boolean whether the value is valid.
* @throws InvalidConfigException if [[compareValue]] is not set.
*/
public function validateValue($value)
{
if ($this->compareValue === null) {
throw new InvalidConfigException('CompareValidator::compareValue must be set.');
} }
break;
default: switch ($this->operator) {
throw new InvalidConfigException("Unknown operator: {$this->operator}"); case '==': return $value == $this->compareValue;
case '===': return $value === $this->compareValue;
case '!=': return $value != $this->compareValue;
case '!==': return $value !== $this->compareValue;
case '>': return $value > $this->compareValue;
case '>=': return $value >= $this->compareValue;
case '<': return $value < $this->compareValue;
case '<=': return $value <= $this->compareValue;
} }
} }
@ -150,57 +180,14 @@ class CompareValidator extends Validator
$compareValue = "\$('#" . (CHtml::activeId($object, $compareAttribute)) . "').val()"; $compareValue = "\$('#" . (CHtml::activeId($object, $compareAttribute)) . "').val()";
$compareLabel = $object->getAttributeLabel($compareAttribute); $compareLabel = $object->getAttributeLabel($compareAttribute);
} }
$condition = "value {$this->operator} $compareValue";
$message = $this->message; $message = strtr($this->message, array(
switch ($this->operator) {
case '=':
case '==':
if ($message === null) {
$message = Yii::t('yii|{attribute} must be repeated exactly.');
}
$condition = 'value!=' . $compareValue;
break;
case '!=':
if ($message === null) {
$message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".');
}
$condition = 'value==' . $compareValue;
break;
case '>':
if ($message === null) {
$message = Yii::t('yii|{attribute} must be greater than "{compareValue}".');
}
$condition = 'value<=' . $compareValue;
break;
case '>=':
if ($message === null) {
$message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".');
}
$condition = 'value<' . $compareValue;
break;
case '<':
if ($message === null) {
$message = Yii::t('yii|{attribute} must be less than "{compareValue}".');
}
$condition = 'value>=' . $compareValue;
break;
case '<=':
if ($message === null) {
$message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".');
}
$condition = 'value>' . $compareValue;
break;
default:
throw new InvalidConfigException("Unknown operator: {$this->operator}");
}
$message = strtr($message, array(
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{compareValue}' => $compareLabel, '{compareValue}' => $compareLabel,
)); ));
return " return "
if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") {
messages.push(" . json_encode($message) . "); messages.push(" . json_encode($message) . ");
} }
"; ";

64
framework/validators/DateValidator.php

@ -7,11 +7,11 @@
namespace yii\validators; namespace yii\validators;
use Yii;
use DateTime;
/** /**
* DateValidator verifies if the attribute represents a date, time or datetime. * DateValidator verifies if the attribute represents a date, time or datetime in a proper format.
*
* By setting the {@link format} property, one can specify what format the date value
* must be in. If the given date value doesn't follow the format, the attribute is considered as invalid.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -19,17 +19,11 @@ namespace yii\validators;
class DateValidator extends Validator class DateValidator extends Validator
{ {
/** /**
* @var mixed the format pattern that the date value should follow. * @var string the date format that the value being validated should follow.
* This can be either a string or an array representing multiple formats. * Please refer to [[http://www.php.net/manual/en/datetime.createfromformat.php]] on
* Defaults to 'MM/dd/yyyy'. Please see {@link CDateTimeParser} for details * supported formats.
* about how to specify a date format.
*/
public $format = 'MM/dd/yyyy';
/**
* @var boolean whether the attribute value can be null or empty. Defaults to true,
* meaning that if the attribute is empty, it is considered valid.
*/ */
public $allowEmpty = true; public $format = 'Y-m-d';
/** /**
* @var string the name of the attribute to receive the parsing result. * @var string the name of the attribute to receive the parsing result.
* When this property is not null and the validation is successful, the named attribute will * When this property is not null and the validation is successful, the named attribute will
@ -38,6 +32,17 @@ class DateValidator extends Validator
public $timestampAttribute; public $timestampAttribute;
/** /**
* Initializes the validator.
*/
public function init()
{
parent::init();
if ($this->message === null) {
$this->message = Yii::t('yii|The format of {attribute} is invalid.');
}
}
/**
* Validates the attribute of the object. * Validates the attribute of the object.
* If there is any error, the error message is added to the object. * If there is any error, the error message is added to the object.
* @param \yii\base\Model $object the object being validated * @param \yii\base\Model $object the object being validated
@ -46,27 +51,26 @@ class DateValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) { if (is_array($value)) {
$this->addError($object, $attribute, $this->message);
return; return;
} }
$date = DateTime::createFromFormat($this->format, $value);
$formats = is_string($this->format) ? array($this->format) : $this->format; if ($date === false) {
$valid = false; $this->addError($object, $attribute, $this->message);
foreach ($formats as $format) { } elseif ($this->timestampAttribute !== false) {
$timestamp = CDateTimeParser::parse($value, $format, array('month' => 1, 'day' => 1, 'hour' => 0, 'minute' => 0, 'second' => 0)); $object->{$this->timestampAttribute} = $date->getTimestamp();
if ($timestamp !== false) {
$valid = true;
if ($this->timestampAttribute !== null) {
$object-> {$this->timestampAttribute} = $timestamp;
}
break;
} }
} }
if (!$valid) { /**
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|The format of {attribute} is invalid.'); * Validates the given value.
$this->addError($object, $attribute, $message); * @param mixed $value the value to be validated.
} * @return boolean whether the value is valid.
*/
public function validateValue($value)
{
return DateTime::createFromFormat($this->format, $value) !== false;
} }
} }

15
framework/validators/DefaultValueValidator.php

@ -10,12 +10,8 @@ namespace yii\validators;
/** /**
* DefaultValueValidator sets the attribute to be the specified default value. * DefaultValueValidator sets the attribute to be the specified default value.
* *
* By default, when the attribute being validated is [[isEmpty|empty]], the validator
* will assign a default [[value]] to it. However, if [[setOnEmpty]] is false, the validator
* will always assign the default [[value]] to the attribute, no matter it is empty or not.
*
* DefaultValueValidator is not really a validator. It is provided mainly to allow * DefaultValueValidator is not really a validator. It is provided mainly to allow
* specifying attribute default values in a dynamic way. * specifying attribute default values when they are empty.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -27,11 +23,10 @@ class DefaultValueValidator extends Validator
*/ */
public $value; public $value;
/** /**
* @var boolean whether to set the default [[value]] only when the attribute is [[isEmpty|empty]]. * @var boolean this property is overwritten to be false so that this validator will
* Defaults to true. If false, the attribute will always be assigned with the default [[value]], * be applied when the value being validated is empty.
* no matter it is empty or not.
*/ */
public $setOnEmpty = true; public $skipOnEmpty = false;
/** /**
* Validates the attribute of the object. * Validates the attribute of the object.
@ -40,7 +35,7 @@ class DefaultValueValidator extends Validator
*/ */
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
if (!$this->setOnEmpty || $this->isEmpty($object->$attribute)) { if ($this->isEmpty($object->$attribute)) {
$object->$attribute = $this->value; $object->$attribute = $this->value;
} }
} }

33
framework/validators/EmailValidator.php

@ -7,6 +7,8 @@
namespace yii\validators; namespace yii\validators;
use Yii;
/** /**
* EmailValidator validates that the attribute value is a valid email address. * EmailValidator validates that the attribute value is a valid email address.
* *
@ -42,11 +44,17 @@ class EmailValidator extends Validator
* Defaults to false. * Defaults to false.
*/ */
public $checkPort = false; public $checkPort = false;
/** /**
* @var boolean whether the attribute value can be null or empty. Defaults to true, * Initializes the validator.
* meaning that if the attribute is empty, it is considered valid.
*/ */
public $allowEmpty = true; public function init()
{
parent::init();
if ($this->message === null) {
$this->message = Yii::t('yii|{attribute} is not a valid email address.');
}
}
/** /**
* Validates the attribute of the object. * Validates the attribute of the object.
@ -57,21 +65,15 @@ class EmailValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) {
return;
}
if (!$this->validateValue($value)) { if (!$this->validateValue($value)) {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message);
} }
} }
/** /**
* Validates a static value to see if it is a valid email. * Validates the given value.
* Note that this method does not respect [[allowEmpty]] property. * @param mixed $value the value to be validated.
* This method is provided so that you can call it directly without going through the model validation rule mechanism. * @return boolean whether the value is valid.
* @param mixed $value the value to be validated
* @return boolean whether the value is a valid email
*/ */
public function validateValue($value) public function validateValue($value)
{ {
@ -98,8 +100,7 @@ class EmailValidator extends Validator
*/ */
public function clientValidateAttribute($object, $attribute) public function clientValidateAttribute($object, $attribute)
{ {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); $message = strtr($this->message, array(
$message = strtr($message, array(
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{value}' => $object->$attribute, '{value}' => $object->$attribute,
)); ));
@ -110,7 +111,7 @@ class EmailValidator extends Validator
} }
return " return "
if(" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") {
messages.push(" . json_encode($message) . "); messages.push(" . json_encode($message) . ");
} }
"; ";

58
framework/validators/ExistValidator.php

@ -6,6 +6,8 @@
*/ */
namespace yii\validators; namespace yii\validators;
use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
/** /**
@ -34,11 +36,18 @@ class ExistValidator extends Validator
* @see className * @see className
*/ */
public $attributeName; public $attributeName;
/** /**
* @var boolean whether the attribute value can be null or empty. Defaults to true, * Initializes the validator.
* meaning that if the attribute is empty, it is considered valid.
*/ */
public $allowEmpty = true; public function init()
{
parent::init();
if ($this->message === null) {
$this->message = Yii::t('yii|{attribute} is invalid.');
}
}
/** /**
* Validates the attribute of the object. * Validates the attribute of the object.
@ -46,29 +55,48 @@ class ExistValidator extends Validator
* *
* @param \yii\db\ActiveRecord $object the object being validated * @param \yii\db\ActiveRecord $object the object being validated
* @param string $attribute the attribute being validated * @param string $attribute the attribute being validated
* @throws InvalidConfigException if table doesn't have column specified
*/ */
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) {
if (is_array($value)) {
$this->addError($object, $attribute, $this->message);
return; return;
} }
/** @var $className \yii\db\ActiveRecord */ /** @var $className \yii\db\ActiveRecord */
$className = ($this->className === null) ? get_class($object) : \Yii::import($this->className); $className = $this->className === null ? get_class($object) : Yii::import($this->className);
$attributeName = ($this->attributeName === null) ? $attribute : $this->attributeName; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
$table = $className::getTableSchema();
if (($column = $table->getColumn($attributeName)) === null) {
throw new InvalidConfigException('Table "' . $table->name . '" does not have a column named "' . $attributeName . '"');
}
$query = $className::find(); $query = $className::find();
$query->where(array($column->name => $value)); $query->where(array($attributeName => $value));
if (!$query->exists()) { if (!$query->exists()) {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} "{value}" is invalid.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message); }
} }
/**
* Validates the given value.
* @param mixed $value the value to be validated.
* @return boolean whether the value is valid.
* @throws InvalidConfigException if either [[className]] or [[attributeName]] is not set.
*/
public function validateValue($value)
{
if (is_array($value)) {
return false;
}
if ($this->className === null) {
throw new InvalidConfigException('The "className" property must be set.');
}
if ($this->attributeName === null) {
throw new InvalidConfigException('The "attributeName" property must be set.');
}
/** @var $className \yii\db\ActiveRecord */
$className = $this->className;
$query = $className::find();
$query->where(array($this->attributeName => $value));
return $query->exists();
} }
} }

277
framework/validators/FileValidator.php

@ -7,47 +7,19 @@
namespace yii\validators; namespace yii\validators;
use Yii;
use yii\helpers\FileHelper;
use yii\web\UploadedFile;
/** /**
* CFileValidator verifies if an attribute is receiving a valid uploaded file. * FileValidator verifies if an attribute is receiving a valid uploaded file.
*
* It uses the model class and attribute name to retrieve the information
* about the uploaded file. It then checks if a file is uploaded successfully,
* if the file size is within the limit and if the file type is allowed.
*
* This validator will attempt to fetch uploaded data if attribute is not
* previously set. Please note that this cannot be done if input is tabular:
* <pre>
* foreach($models as $i=>$model)
* $model->attribute = CUploadedFile::getInstance($model, "[$i]attribute");
* </pre>
* Please note that you must use {@link CUploadedFile::getInstances} for multiple
* file uploads.
*
* When using CFileValidator with an active record, the following code is often used:
* <pre>
* if($model->save())
* {
* // single upload
* $model->attribute->saveAs($path);
* // multiple upload
* foreach($model->attribute as $file)
* $file->saveAs($path);
* }
* </pre>
*
* You can use {@link CFileValidator} to validate the file attribute.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class CFileValidator extends Validator class FileValidator extends Validator
{ {
/** /**
* @var boolean whether the attribute requires a file to be uploaded or not.
* Defaults to false, meaning a file is required to be uploaded.
*/
public $allowEmpty = false;
/**
* @var mixed a list of file name extensions that are allowed to be uploaded. * @var mixed a list of file name extensions that are allowed to be uploaded.
* This can be either an array or a string consisting of file extension names * This can be either an array or a string consisting of file extension names
* separated by space or comma (e.g. "gif, jpg"). * separated by space or comma (e.g. "gif, jpg").
@ -66,68 +38,124 @@ class CFileValidator extends Validator
* Defaults to null, meaning no limit. * Defaults to null, meaning no limit.
* Note, the size limit is also affected by 'upload_max_filesize' INI setting * Note, the size limit is also affected by 'upload_max_filesize' INI setting
* and the 'MAX_FILE_SIZE' hidden field value. * and the 'MAX_FILE_SIZE' hidden field value.
* @see tooLarge * @see tooBig
*/ */
public $maxSize; public $maxSize;
/** /**
* @var integer the maximum file count the given attribute can hold.
* It defaults to 1, meaning single file upload. By defining a higher number,
* multiple uploads become possible.
*/
public $maxFiles = 1;
/**
* @var string the error message used when a file is not uploaded correctly.
*/
public $message;
/**
* @var string the error message used when no file is uploaded.
*/
public $uploadRequired;
/**
* @var string the error message used when the uploaded file is too large. * @var string the error message used when the uploaded file is too large.
* @see maxSize * You may use the following tokens in the message:
*
* - {attribute}: the attribute name
* - {file}: the uploaded file name
* - {limit}: the maximum size allowed (see [[getSizeLimit()]])
*/ */
public $tooLarge; public $tooBig;
/** /**
* @var string the error message used when the uploaded file is too small. * @var string the error message used when the uploaded file is too small.
* @see minSize * You may use the following tokens in the message:
*
* - {attribute}: the attribute name
* - {file}: the uploaded file name
* - {limit}: the value of [[minSize]]
*/ */
public $tooSmall; public $tooSmall;
/** /**
* @var string the error message used when the uploaded file has an extension name * @var string the error message used when the uploaded file has an extension name
* that is not listed among {@link extensions}. * that is not listed in [[extensions]]. You may use the following tokens in the message:
*
* - {attribute}: the attribute name
* - {extensions}: the list of the allowed extensions.
*/ */
public $wrongType; public $wrongType;
/** /**
* @var integer the maximum file count the given attribute can hold. * @var string the error message used if the count of multiple uploads exceeds limit.
* It defaults to 1, meaning single file upload. By defining a higher number, * You may use the following tokens in the message:
* multiple uploads become possible. *
* - {attribute}: the attribute name
* - {file}: the uploaded file name
* - {limit}: the value of [[maxFiles]]
*/ */
public $maxFiles = 1; public $tooMany;
/** /**
* @var string the error message used if the count of multiple uploads exceeds * Initializes the validator.
* limit.
*/ */
public $tooMany; public function init()
{
parent::init();
if ($this->message === null) {
$this->message = Yii::t('yii|File upload failed.');
}
if ($this->uploadRequired === null) {
$this->uploadRequired = Yii::t('yii|Please upload a file.');
}
if ($this->tooMany === null) {
$this->tooMany = Yii::t('yii|You can upload at most {limit} files.');
}
if ($this->wrongType === null) {
$this->wrongType = Yii::t('yii|Only files with these extensions are allowed: {extensions}.');
}
if ($this->tooBig === null) {
$this->tooBig = Yii::t('yii|The file "{file}" is too big. Its size cannot exceed {limit} bytes.');
}
if ($this->tooSmall === null) {
$this->tooSmall = Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.');
}
if (!is_array($this->types)) {
$this->types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY);
}
}
/** /**
* Set the attribute and then validates using {@link validateFile}. * Validates the attribute.
* If there is any error, the error message is added to the object.
* @param \yii\base\Model $object the object being validated * @param \yii\base\Model $object the object being validated
* @param string $attribute the attribute being validated * @param string $attribute the attribute being validated
*/ */
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
if ($this->maxFiles > 1) if ($this->maxFiles > 1) {
{
$files = $object->$attribute; $files = $object->$attribute;
if (!is_array($files) || !isset($files[0]) || !$files[0] instanceof CUploadedFile) if (!is_array($files)) {
$files = CUploadedFile::getInstances($object, $attribute); $this->addError($object, $attribute, $this->uploadRequired);
if (array() === $files) return;
return $this->emptyAttribute($object, $attribute); }
if (count($files) > $this->maxFiles) foreach ($files as $i => $file) {
{ if (!$file instanceof UploadedFile || $file->getError() == UPLOAD_ERR_NO_FILE) {
$message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii|{attribute} cannot accept more than {limit} files.'); unset($files[$i]);
$this->addError($object, $attribute, $message, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); }
} else }
foreach ($files as $file) $object->$attribute = array_values($files);
if ($files === array()) {
$this->addError($object, $attribute, $this->uploadRequired);
}
if (count($files) > $this->maxFiles) {
$this->addError($object, $attribute, $this->tooMany, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles));
} else {
foreach ($files as $file) {
$this->validateFile($object, $attribute, $file); $this->validateFile($object, $attribute, $file);
} else
{
$file = $object->$attribute;
if (!$file instanceof CUploadedFile)
{
$file = CUploadedFile::getInstance($object, $attribute);
if (null === $file)
return $this->emptyAttribute($object, $attribute);
} }
}
} else {
$file = $object->$attribute;
if ($file instanceof UploadedFile && $file->getError() != UPLOAD_ERR_NO_FILE) {
$this->validateFile($object, $attribute, $file); $this->validateFile($object, $attribute, $file);
} else {
$this->addError($object, $attribute, $this->uploadRequired);
}
} }
} }
@ -135,67 +163,54 @@ class CFileValidator extends Validator
* Internally validates a file object. * Internally validates a file object.
* @param \yii\base\Model $object the object being validated * @param \yii\base\Model $object the object being validated
* @param string $attribute the attribute being validated * @param string $attribute the attribute being validated
* @param CUploadedFile $file uploaded file passed to check against a set of rules * @param UploadedFile $file uploaded file passed to check against a set of rules
*/ */
public function validateFile($object, $attribute, $file) protected function validateFile($object, $attribute, $file)
{ {
if (null === $file || ($error = $file->getError()) == UPLOAD_ERR_NO_FILE) switch ($file->getError()) {
return $this->emptyAttribute($object, $attribute); case UPLOAD_ERR_OK:
elseif ($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE || $this->maxSize !== null && $file->getSize() > $this->maxSize) if ($this->maxSize !== null && $file->getSize() > $this->maxSize) {
{ $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit()));
$message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii|The file "{file}" is too large. Its size cannot exceed {limit} bytes.');
$this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit()));
} elseif ($error == UPLOAD_ERR_PARTIAL)
throw new CException(\Yii::t('yii|The file "{file}" was only partially uploaded.', array('{file}' => $file->getName())));
elseif ($error == UPLOAD_ERR_NO_TMP_DIR)
throw new CException(\Yii::t('yii|Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName())));
elseif ($error == UPLOAD_ERR_CANT_WRITE)
throw new CException(\Yii::t('yii|Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName())));
elseif (defined('UPLOAD_ERR_EXTENSION') && $error == UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above
throw new CException(\Yii::t('yii|File upload was stopped by extension.'));
if ($this->minSize !== null && $file->getSize() < $this->minSize)
{
$message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.');
$this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->minSize));
}
if ($this->types !== null)
{
if (is_string($this->types))
$types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY);
else
$types = $this->types;
if (!in_array(strtolower($file->getExtensionName()), $types))
{
$message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii|The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.');
$this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $types)));
} }
if ($this->minSize !== null && $file->getSize() < $this->minSize) {
$this->addError($object, $attribute, $this->tooSmall, array('{file}' => $file->getName(), '{limit}' => $this->minSize));
} }
if (!empty($this->types) && !in_array(strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION)), $this->types, true)) {
$this->addError($object, $attribute, $this->wrongType, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $this->types)));
} }
break;
/** case UPLOAD_ERR_INI_SIZE:
* Raises an error to inform end user about blank attribute. case UPLOAD_ERR_FORM_SIZE:
* @param \yii\base\Model $object the object being validated $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit()));
* @param string $attribute the attribute being validated break;
*/ case UPLOAD_ERR_PARTIAL:
public function emptyAttribute($object, $attribute) $this->addError($object, $attribute, $this->message);
{ Yii::warning('File was only partially uploaded: ' . $file->getName(), __METHOD__);
if (!$this->allowEmpty) break;
{ case UPLOAD_ERR_NO_TMP_DIR:
$message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message); Yii::warning('Missing the temporary folder to store the uploaded file: ' . $file->getName(), __METHOD__);
break;
case UPLOAD_ERR_CANT_WRITE:
$this->addError($object, $attribute, $this->message);
Yii::warning('Failed to write the uploaded file to disk: ', $file->getName(), __METHOD__);
break;
case UPLOAD_ERR_EXTENSION:
$this->addError($object, $attribute, $this->message);
Yii::warning('File upload was stopped by some PHP extension: ', $file->getName(), __METHOD__);
break;
default:
break;
} }
} }
/** /**
* Returns the maximum size allowed for uploaded files. * Returns the maximum size allowed for uploaded files.
* This is determined based on three factors: * This is determined based on three factors:
* <ul> *
* <li>'upload_max_filesize' in php.ini</li> * - 'upload_max_filesize' in php.ini
* <li>'MAX_FILE_SIZE' hidden field</li> * - 'MAX_FILE_SIZE' hidden field
* <li>{@link maxSize}</li> * - [[maxSize]]
* </ul>
* *
* @return integer the size limit for uploaded files. * @return integer the size limit for uploaded files.
*/ */
@ -203,10 +218,12 @@ class CFileValidator extends Validator
{ {
$limit = ini_get('upload_max_filesize'); $limit = ini_get('upload_max_filesize');
$limit = $this->sizeToBytes($limit); $limit = $this->sizeToBytes($limit);
if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) {
$limit = $this->maxSize; $limit = $this->maxSize;
if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) }
$limit = $_POST['MAX_FILE_SIZE']; if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) {
$limit = (int)$_POST['MAX_FILE_SIZE'];
}
return $limit; return $limit;
} }
@ -218,12 +235,18 @@ class CFileValidator extends Validator
*/ */
private function sizeToBytes($sizeStr) private function sizeToBytes($sizeStr)
{ {
switch (substr($sizeStr, -1)) switch (substr($sizeStr, -1)) {
{ case 'M':
case 'M': case 'm': return (int)$sizeStr * 1048576; case 'm':
case 'K': case 'k': return (int)$sizeStr * 1024; return (int)$sizeStr * 1048576;
case 'G': case 'g': return (int)$sizeStr * 1073741824; case 'K':
default: return (int)$sizeStr; case 'k':
return (int)$sizeStr * 1024;
case 'G':
case 'g':
return (int)$sizeStr * 1073741824;
default:
return (int)$sizeStr;
} }
} }
} }

20
framework/validators/FilterValidator.php

@ -38,6 +38,23 @@ class FilterValidator extends Validator
* ~~~ * ~~~
*/ */
public $filter; public $filter;
/**
* @var boolean this property is overwritten to be false so that this validator will
* be applied when the value being validated is empty.
*/
public $skipOnEmpty = false;
/**
* Initializes the validator.
* @throws InvalidConfigException if [[filter]] is not set.
*/
public function init()
{
parent::init();
if ($this->filter === null) {
throw new InvalidConfigException('The "filter" property must be set.');
}
}
/** /**
* Validates the attribute of the object. * Validates the attribute of the object.
@ -48,9 +65,6 @@ class FilterValidator extends Validator
*/ */
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
if ($this->filter === null) {
throw new InvalidConfigException('The "filter" property must be specified with a valid callback.');
}
$object->$attribute = call_user_func($this->filter, $object->$attribute); $object->$attribute = call_user_func($this->filter, $object->$attribute);
} }
} }

21
framework/validators/InlineValidator.php

@ -25,8 +25,9 @@ namespace yii\validators;
class InlineValidator extends Validator class InlineValidator extends Validator
{ {
/** /**
* @var string the name of the validation method defined in the * @var string|\Closure an anonymous function or the name of a model class method that will be
* \yii\base\Model class * called to perform the actual validation. Note that if you use anonymous function, you cannot
* use `$this` in it unless you are using PHP 5.4 or above.
*/ */
public $method; public $method;
/** /**
@ -34,8 +35,8 @@ class InlineValidator extends Validator
*/ */
public $params; public $params;
/** /**
* @var string the name of the method that returns the client validation code (see [[clientValidateAttribute()]] * @var string|\Closure an anonymous function or the name of a model class method that returns the client validation code.
* for details on how to return client validation code). The signature of the method should be like the following: * The signature of the method should be like the following:
* *
* ~~~ * ~~~
* function foo($attribute) * function foo($attribute)
@ -45,6 +46,8 @@ class InlineValidator extends Validator
* ~~~ * ~~~
* *
* where `$attribute` refers to the attribute name to be validated. * where `$attribute` refers to the attribute name to be validated.
*
* Please refer to [[clientValidateAttribute()]] for details on how to return client validation code.
*/ */
public $clientValidate; public $clientValidate;
@ -56,7 +59,10 @@ class InlineValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$method = $this->method; $method = $this->method;
$object->$method($attribute, $this->params); if (is_string($method)) {
$method = array($object, $method);
}
call_user_func($method, $attribute, $this->params);
} }
/** /**
@ -82,7 +88,10 @@ class InlineValidator extends Validator
{ {
if ($this->clientValidate !== null) { if ($this->clientValidate !== null) {
$method = $this->clientValidate; $method = $this->clientValidate;
return $object->$method($attribute); if (is_string($method)) {
$method = array($object, $method);
}
return call_user_func($method, $attribute);
} else { } else {
return null; return null;
} }

76
framework/validators/NumberValidator.php

@ -26,11 +26,6 @@ class NumberValidator extends Validator
*/ */
public $integerOnly = false; public $integerOnly = false;
/** /**
* @var boolean whether the attribute value can be null or empty. Defaults to true,
* meaning that if the attribute is empty, it is considered valid.
*/
public $allowEmpty = true;
/**
* @var integer|float upper limit of the number. Defaults to null, meaning no upper limit. * @var integer|float upper limit of the number. Defaults to null, meaning no upper limit.
*/ */
public $max; public $max;
@ -58,6 +53,24 @@ class NumberValidator extends Validator
/** /**
* Initializes the validator.
*/
public function init()
{
parent::init();
if ($this->message === null) {
$this->message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.')
: Yii::t('yii|{attribute} must be a number.');
}
if ($this->min !== null && $this->tooSmall === null) {
$this->tooSmall = Yii::t('yii|{attribute} must be no less than {min}.');
}
if ($this->max !== null && $this->tooBig === null) {
$this->tooBig = Yii::t('yii|{attribute} must be no greater than {max}.');
}
}
/**
* Validates the attribute of the object. * Validates the attribute of the object.
* If there is any error, the error message is added to the object. * If there is any error, the error message is added to the object.
* @param \yii\base\Model $object the object being validated * @param \yii\base\Model $object the object being validated
@ -66,28 +79,32 @@ class NumberValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) { if (is_array($value)) {
$this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.'));
return; return;
} }
if ($this->integerOnly) { $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern;
if (!preg_match($this->integerPattern, "$value")) { if (!preg_match($pattern, "$value")) {
$message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be an integer.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message);
}
} else {
if (!preg_match($this->numberPattern, "$value")) {
$message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be a number.');
$this->addError($object, $attribute, $message);
}
} }
if ($this->min !== null && $value < $this->min) { if ($this->min !== null && $value < $this->min) {
$message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii|{attribute} is too small (minimum is {min}).'); $this->addError($object, $attribute, $this->tooSmall, array('{min}' => $this->min));
$this->addError($object, $attribute, $message, array('{min}' => $this->min));
} }
if ($this->max !== null && $value > $this->max) { if ($this->max !== null && $value > $this->max) {
$message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii|{attribute} is too big (maximum is {max}).'); $this->addError($object, $attribute, $this->tooBig, array('{max}' => $this->max));
$this->addError($object, $attribute, $message, array('{max}' => $this->max)); }
} }
/**
* Validates the given value.
* @param mixed $value the value to be validated.
* @return boolean whether the value is valid.
*/
public function validateValue($value)
{
return preg_match($this->integerOnly ? $this->integerPattern : $this->numberPattern, "$value")
&& ($this->min === null || $value >= $this->min)
&& ($this->max === null || $value <= $this->max);
} }
/** /**
@ -99,12 +116,7 @@ class NumberValidator extends Validator
public function clientValidateAttribute($object, $attribute) public function clientValidateAttribute($object, $attribute)
{ {
$label = $object->getAttributeLabel($attribute); $label = $object->getAttributeLabel($attribute);
$message = strtr($this->message, array(
if (($message = $this->message) === null) {
$message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.')
: Yii::t('yii|{attribute} must be a number.');
}
$message = strtr($message, array(
'{attribute}' => $label, '{attribute}' => $label,
)); ));
@ -115,10 +127,7 @@ if(!value.match($pattern)) {
} }
"; ";
if ($this->min !== null) { if ($this->min !== null) {
if (($tooSmall = $this->tooSmall) === null) { $tooSmall = strtr($this->tooSmall, array(
$tooSmall = Yii::t('yii|{attribute} is too small (minimum is {min}).');
}
$tooSmall = strtr($tooSmall, array(
'{attribute}' => $label, '{attribute}' => $label,
'{min}' => $this->min, '{min}' => $this->min,
)); ));
@ -130,10 +139,7 @@ if(value<{$this->min}) {
"; ";
} }
if ($this->max !== null) { if ($this->max !== null) {
if (($tooBig = $this->tooBig) === null) { $tooBig = strtr($this->tooBig, array(
$tooBig = Yii::t('yii|{attribute} is too big (maximum is {max}).');
}
$tooBig = strtr($tooBig, array(
'{attribute}' => $label, '{attribute}' => $label,
'{max}' => $this->max, '{max}' => $this->max,
)); ));
@ -144,7 +150,7 @@ if(value>{$this->max}) {
"; ";
} }
if ($this->allowEmpty) { if ($this->skipOnEmpty) {
$js = " $js = "
if(jQuery.trim(value)!='') { if(jQuery.trim(value)!='') {
$js $js

58
framework/validators/RangeValidator.php

@ -6,6 +6,8 @@
*/ */
namespace yii\validators; namespace yii\validators;
use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
/** /**
@ -29,58 +31,62 @@ class RangeValidator extends Validator
*/ */
public $strict = false; public $strict = false;
/** /**
* @var boolean whether the attribute value can be null or empty. Defaults to true,
* meaning that if the attribute is empty, it is considered valid.
*/
public $allowEmpty = true;
/**
* @var boolean whether to invert the validation logic. Defaults to false. If set to true, * @var boolean whether to invert the validation logic. Defaults to false. If set to true,
* the attribute value should NOT be among the list of values defined via [[range]]. * the attribute value should NOT be among the list of values defined via [[range]].
**/ **/
public $not = false; public $not = false;
/** /**
* Initializes the validator.
* @throws InvalidConfigException if [[range]] is not set.
*/
public function init()
{
parent::init();
if (!is_array($this->range)) {
throw new InvalidConfigException('The "range" property must be set.');
}
if ($this->message === null) {
$this->message = Yii::t('yii|{attribute} is invalid.');
}
}
/**
* Validates the attribute of the object. * Validates the attribute of the object.
* If there is any error, the error message is added to the object. * If there is any error, the error message is added to the object.
* @param \yii\base\Model $object the object being validated * @param \yii\base\Model $object the object being validated
* @param string $attribute the attribute being validated * @param string $attribute the attribute being validated
* @throws InvalidConfigException if the "range" property is not an array
*/ */
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) {
return;
}
if (!is_array($this->range)) {
throw new InvalidConfigException('The "range" property must be specified as an array.');
}
if (!$this->not && !in_array($value, $this->range, $this->strict)) { if (!$this->not && !in_array($value, $this->range, $this->strict)) {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should be in the list.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message);
} elseif ($this->not && in_array($value, $this->range, $this->strict)) { } elseif ($this->not && in_array($value, $this->range, $this->strict)) {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should NOT be in the list.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message);
} }
} }
/** /**
* Validates the given value.
* @param mixed $value the value to be validated.
* @return boolean whether the value is valid.
*/
public function validateValue($value)
{
return !$this->not && in_array($value, $this->range, $this->strict)
|| $this->not && !in_array($value, $this->range, $this->strict);
}
/**
* Returns the JavaScript needed for performing client-side validation. * Returns the JavaScript needed for performing client-side validation.
* @param \yii\base\Model $object the data object being validated * @param \yii\base\Model $object the data object being validated
* @param string $attribute the name of the attribute to be validated. * @param string $attribute the name of the attribute to be validated.
* @return string the client-side validation script. * @return string the client-side validation script.
* @throws InvalidConfigException if the "range" property is not an array
*/ */
public function clientValidateAttribute($object, $attribute) public function clientValidateAttribute($object, $attribute)
{ {
if (!is_array($this->range)) { $message = strtr($this->message, array(
throw new InvalidConfigException('The "range" property must be specified as an array.');
}
if (($message = $this->message) === null) {
$message = $this->not ? \Yii::t('yii|{attribute} should NOT be in the list.') : \Yii::t('yii|{attribute} should be in the list.');
}
$message = strtr($message, array(
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{value}' => $object->$attribute, '{value}' => $object->$attribute,
)); ));
@ -92,7 +98,7 @@ class RangeValidator extends Validator
$range = json_encode($range); $range = json_encode($range);
return " return "
if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? "$.inArray(value, $range)>=0" : "$.inArray(value, $range)<0") . ") { if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? "$.inArray(value, $range)>=0" : "$.inArray(value, $range)<0") . ") {
messages.push(" . json_encode($message) . "); messages.push(" . json_encode($message) . ");
} }
"; ";

57
framework/validators/RegularExpressionValidator.php

@ -7,6 +7,9 @@
namespace yii\validators; namespace yii\validators;
use Yii;
use yii\base\InvalidConfigException;
/** /**
* RegularExpressionValidator validates that the attribute value matches the specified [[pattern]]. * RegularExpressionValidator validates that the attribute value matches the specified [[pattern]].
* *
@ -22,36 +25,51 @@ class RegularExpressionValidator extends Validator
*/ */
public $pattern; public $pattern;
/** /**
* @var boolean whether the attribute value can be null or empty. Defaults to true,
* meaning that if the attribute is empty, it is considered valid.
*/
public $allowEmpty = true;
/**
* @var boolean whether to invert the validation logic. Defaults to false. If set to true, * @var boolean whether to invert the validation logic. Defaults to false. If set to true,
* the regular expression defined via [[pattern]] should NOT match the attribute value. * the regular expression defined via [[pattern]] should NOT match the attribute value.
* @throws InvalidConfigException if the "pattern" is not a valid regular expression
**/ **/
public $not = false; public $not = false;
/** /**
* Initializes the validator.
* @throws InvalidConfigException if [[pattern]] is not set.
*/
public function init()
{
parent::init();
if ($this->pattern === null) {
throw new InvalidConfigException('The "pattern" property must be set.');
}
if ($this->message === null) {
$this->message = Yii::t('yii|{attribute} is invalid.');
}
}
/**
* Validates the attribute of the object. * Validates the attribute of the object.
* If there is any error, the error message is added to the object. * If there is any error, the error message is added to the object.
* @param \yii\base\Model $object the object being validated * @param \yii\base\Model $object the object being validated
* @param string $attribute the attribute being validated * @param string $attribute the attribute being validated
* @throws \yii\base\Exception if the "pattern" is not a valid regular expression
*/ */
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) { if (!$this->validateValue($value)) {
return; $this->addError($object, $attribute, $this->message);
}
if ($this->pattern === null) {
throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.');
} }
if ((!$this->not && !preg_match($this->pattern, $value)) || ($this->not && preg_match($this->pattern, $value))) {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.');
$this->addError($object, $attribute, $message);
} }
/**
* Validates the given value.
* @param mixed $value the value to be validated.
* @return boolean whether the value is valid.
*/
public function validateValue($value)
{
return !is_array($value) &&
(!$this->not && preg_match($this->pattern, $value)
|| $this->not && !preg_match($this->pattern, $value));
} }
/** /**
@ -59,16 +77,11 @@ class RegularExpressionValidator extends Validator
* @param \yii\base\Model $object the data object being validated * @param \yii\base\Model $object the data object being validated
* @param string $attribute the name of the attribute to be validated. * @param string $attribute the name of the attribute to be validated.
* @return string the client-side validation script. * @return string the client-side validation script.
* @throws \yii\base\Exception if the "pattern" is not a valid regular expression * @throws InvalidConfigException if the "pattern" is not a valid regular expression
*/ */
public function clientValidateAttribute($object, $attribute) public function clientValidateAttribute($object, $attribute)
{ {
if ($this->pattern === null) { $message = strtr($this->message, array(
throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.');
}
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.');
$message = strtr($message, array(
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{value}' => $object->$attribute, '{value}' => $object->$attribute,
)); ));
@ -88,7 +101,7 @@ class RegularExpressionValidator extends Validator
} }
return " return "
if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? '' : '!') . "value.match($pattern)) { if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? '' : '!') . "value.match($pattern)) {
messages.push(" . json_encode($message) . "); messages.push(" . json_encode($message) . ");
} }
"; ";

52
framework/validators/RequiredValidator.php

@ -7,6 +7,8 @@
namespace yii\validators; namespace yii\validators;
use Yii;
/** /**
* RequiredValidator validates that the specified attribute does not have null or empty value. * RequiredValidator validates that the specified attribute does not have null or empty value.
* *
@ -16,6 +18,10 @@ namespace yii\validators;
class RequiredValidator extends Validator class RequiredValidator extends Validator
{ {
/** /**
* @var boolean whether to skip this validator if the value being validated is empty.
*/
public $skipOnEmpty = false;
/**
* @var mixed the desired value that the attribute must have. * @var mixed the desired value that the attribute must have.
* If this is null, the validator will validate that the specified attribute is not empty. * If this is null, the validator will validate that the specified attribute is not empty.
* If this is set as a value that is not null, the validator will validate that * If this is set as a value that is not null, the validator will validate that
@ -35,6 +41,18 @@ class RequiredValidator extends Validator
public $strict = false; public $strict = false;
/** /**
* Initializes the validator.
*/
public function init()
{
parent::init();
if ($this->message === null) {
$this->message = $this->requiredValue === null ? Yii::t('yii|{attribute} is invalid.')
: Yii::t('yii|{attribute} must be "{requiredValue}".');
}
}
/**
* Validates the attribute of the object. * Validates the attribute of the object.
* If there is any error, the error message is added to the object. * If there is any error, the error message is added to the object.
* @param \yii\base\Model $object the object being validated * @param \yii\base\Model $object the object being validated
@ -45,13 +63,11 @@ class RequiredValidator extends Validator
$value = $object->$attribute; $value = $object->$attribute;
if ($this->requiredValue === null) { if ($this->requiredValue === null) {
if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) { if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message);
} }
} else { } else {
if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) { if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be "{requiredValue}".'); $this->addError($object, $attribute, $this->message, array(
$this->addError($object, $attribute, $message, array(
'{requiredValue}' => $this->requiredValue, '{requiredValue}' => $this->requiredValue,
)); ));
} }
@ -59,6 +75,23 @@ class RequiredValidator extends Validator
} }
/** /**
* Validates the given value.
* @param mixed $value the value to be validated.
* @return boolean whether the value is valid.
*/
public function validateValue($value)
{
if ($this->requiredValue === null) {
if ($this->strict && $value !== null || !$this->strict && !$this->isEmpty($value, true)) {
return true;
}
} elseif (!$this->strict && $value == $this->requiredValue || $this->strict && $value === $this->requiredValue) {
return true;
}
return false;
}
/**
* Returns the JavaScript needed for performing client-side validation. * Returns the JavaScript needed for performing client-side validation.
* @param \yii\base\Model $object the data object being validated * @param \yii\base\Model $object the data object being validated
* @param string $attribute the name of the attribute to be validated. * @param string $attribute the name of the attribute to be validated.
@ -66,12 +99,8 @@ class RequiredValidator extends Validator
*/ */
public function clientValidateAttribute($object, $attribute) public function clientValidateAttribute($object, $attribute)
{ {
$message = $this->message;
if ($this->requiredValue !== null) { if ($this->requiredValue !== null) {
if ($message === null) { $message = strtr($this->message, array(
$message = \Yii::t('yii|{attribute} must be "{requiredValue}".');
}
$message = strtr($message, array(
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{value}' => $object->$attribute, '{value}' => $object->$attribute,
'{requiredValue}' => $this->requiredValue, '{requiredValue}' => $this->requiredValue,
@ -82,10 +111,7 @@ if (value != " . json_encode($this->requiredValue) . ") {
} }
"; ";
} else { } else {
if ($message === null) { $message = strtr($this->message, array(
$message = \Yii::t('yii|{attribute} cannot be blank.');
}
$message = strtr($message, array(
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{value}' => $object->$attribute, '{value}' => $object->$attribute,
)); ));

91
framework/validators/StringValidator.php

@ -7,6 +7,8 @@
namespace yii\validators; namespace yii\validators;
use Yii;
/** /**
* StringValidator validates that the attribute value is of certain length. * StringValidator validates that the attribute value is of certain length.
* *
@ -46,19 +48,34 @@ class StringValidator extends Validator
*/ */
public $notEqual; public $notEqual;
/** /**
* @var boolean whether the attribute value can be null or empty. Defaults to true, * @var string the encoding of the string value to be validated (e.g. 'UTF-8').
* meaning that if the attribute is empty, it is considered valid. * If this property is not set, [[\yii\base\Application::charset]] will be used.
*/ */
public $allowEmpty = true; public $encoding;
/** /**
* @var mixed the encoding of the string value to be validated (e.g. 'UTF-8'). * Initializes the validator.
* This property is used only when mbstring PHP extension is enabled.
* The value of this property will be used as the 2nd parameter of the
* mb_strlen() function. If this property is not set, the application charset
* will be used. If this property is set false, then strlen() will be used even
* if mbstring is enabled.
*/ */
public $encoding; public function init()
{
parent::init();
if ($this->encoding === null) {
$this->encoding = Yii::$app->charset;
}
if ($this->message === null) {
$this->message = Yii::t('yii|{attribute} must be a string.');
}
if ($this->min !== null && $this->tooShort === null) {
$this->tooShort = Yii::t('yii|{attribute} should contain at least {min} characters.');
}
if ($this->max !== null && $this->tooLong === null) {
$this->tooLong = Yii::t('yii|{attribute} should contain at most {max} characters.');
}
if ($this->is !== null && $this->notEqual === null) {
$this->notEqual = Yii::t('yii|{attribute} should contain {length} characters.');
}
}
/** /**
* Validates the attribute of the object. * Validates the attribute of the object.
@ -69,34 +86,39 @@ class StringValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) {
return;
}
if (!is_string($value)) { if (!is_string($value)) {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be a string.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message);
return; return;
} }
if (function_exists('mb_strlen') && $this->encoding !== false) { $length = mb_strlen($value, $this->encoding);
$length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$app->charset);
} else {
$length = strlen($value);
}
if ($this->min !== null && $length < $this->min) { if ($this->min !== null && $length < $this->min) {
$message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); $this->addError($object, $attribute, $this->tooShort, array('{min}' => $this->min));
$this->addError($object, $attribute, $message, array('{min}' => $this->min));
} }
if ($this->max !== null && $length > $this->max) { if ($this->max !== null && $length > $this->max) {
$message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); $this->addError($object, $attribute, $this->tooLong, array('{max}' => $this->max));
$this->addError($object, $attribute, $message, array('{max}' => $this->max));
} }
if ($this->is !== null && $length !== $this->is) { if ($this->is !== null && $length !== $this->is) {
$message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); $this->addError($object, $attribute, $this->notEqual, array('{length}' => $this->is));
$this->addError($object, $attribute, $message, array('{length}' => $this->is)); }
}
/**
* Validates the given value.
* @param mixed $value the value to be validated.
* @return boolean whether the value is valid.
*/
public function validateValue($value)
{
if (!is_string($value)) {
return false;
} }
$length = mb_strlen($value, $this->encoding);
return ($this->min === null || $length >= $this->min)
&& ($this->max === null || $length <= $this->max)
&& ($this->is === null || $length === $this->is);
} }
/** /**
@ -110,28 +132,19 @@ class StringValidator extends Validator
$label = $object->getAttributeLabel($attribute); $label = $object->getAttributeLabel($attribute);
$value = $object->$attribute; $value = $object->$attribute;
if (($notEqual = $this->notEqual) === null) { $notEqual = strtr($this->notEqual, array(
$notEqual = \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).');
}
$notEqual = strtr($notEqual, array(
'{attribute}' => $label, '{attribute}' => $label,
'{value}' => $value, '{value}' => $value,
'{length}' => $this->is, '{length}' => $this->is,
)); ));
if (($tooShort = $this->tooShort) === null) { $tooShort = strtr($this->tooShort, array(
$tooShort = \Yii::t('yii|{attribute} is too short (minimum is {min} characters).');
}
$tooShort = strtr($tooShort, array(
'{attribute}' => $label, '{attribute}' => $label,
'{value}' => $value, '{value}' => $value,
'{min}' => $this->min, '{min}' => $this->min,
)); ));
if (($tooLong = $this->tooLong) === null) { $tooLong = strtr($this->tooLong, array(
$tooLong = \Yii::t('yii|{attribute} is too long (maximum is {max} characters).');
}
$tooLong = strtr($tooLong, array(
'{attribute}' => $label, '{attribute}' => $label,
'{value}' => $value, '{value}' => $value,
'{max}' => $this->max, '{max}' => $this->max,
@ -160,7 +173,7 @@ if(value.length!= {$this->is}) {
"; ";
} }
if ($this->allowEmpty) { if ($this->skipOnEmpty) {
$js = " $js = "
if($.trim(value)!='') { if($.trim(value)!='') {
$js $js

29
framework/validators/UniqueValidator.php

@ -6,6 +6,8 @@
*/ */
namespace yii\validators; namespace yii\validators;
use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
/** /**
@ -17,11 +19,6 @@ use yii\base\InvalidConfigException;
class UniqueValidator extends Validator class UniqueValidator extends Validator
{ {
/** /**
* @var boolean whether the attribute value can be null or empty. Defaults to true,
* meaning that if the attribute is empty, it is considered valid.
*/
public $allowEmpty = true;
/**
* @var string the ActiveRecord class name or alias of the class * @var string the ActiveRecord class name or alias of the class
* that should be used to look for the attribute value being validated. * that should be used to look for the attribute value being validated.
* Defaults to null, meaning using the ActiveRecord class of the attribute being validated. * Defaults to null, meaning using the ActiveRecord class of the attribute being validated.
@ -36,6 +33,17 @@ class UniqueValidator extends Validator
public $attributeName; public $attributeName;
/** /**
* Initializes the validator.
*/
public function init()
{
parent::init();
if ($this->message === null) {
$this->message = Yii::t('yii|{attribute} "{value}" has already been taken.');
}
}
/**
* Validates the attribute of the object. * Validates the attribute of the object.
* If there is any error, the error message is added to the object. * If there is any error, the error message is added to the object.
* @param \yii\db\ActiveRecord $object the object being validated * @param \yii\db\ActiveRecord $object the object being validated
@ -45,17 +53,19 @@ class UniqueValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) {
if (is_array($value)) {
$this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.'));
return; return;
} }
/** @var $className \yii\db\ActiveRecord */ /** @var $className \yii\db\ActiveRecord */
$className = $this->className === null ? get_class($object) : \Yii::import($this->className); $className = $this->className === null ? get_class($object) : Yii::import($this->className);
$attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
$table = $className::getTableSchema(); $table = $className::getTableSchema();
if (($column = $table->getColumn($attributeName)) === null) { if (($column = $table->getColumn($attributeName)) === null) {
throw new InvalidConfigException('Table "' . $table->name . '" does not have a column named "' . $attributeName . '"'); throw new InvalidConfigException("Table '{$table->name}' does not have a column named '$attributeName'.");
} }
$query = $className::find(); $query = $className::find();
@ -84,8 +94,7 @@ class UniqueValidator extends Validator
} }
if ($exists) { if ($exists) {
$message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} "{value}" has already been taken.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message);
} }
} }
} }

40
framework/validators/UrlValidator.php

@ -7,6 +7,8 @@
namespace yii\validators; namespace yii\validators;
use Yii;
/** /**
* UrlValidator validates that the attribute value is a valid http or https URL. * UrlValidator validates that the attribute value is a valid http or https URL.
* *
@ -32,11 +34,18 @@ class UrlValidator extends Validator
* contain the scheme part. * contain the scheme part.
**/ **/
public $defaultScheme; public $defaultScheme;
/** /**
* @var boolean whether the attribute value can be null or empty. Defaults to true, * Initializes the validator.
* meaning that if the attribute is empty, it is considered valid.
*/ */
public $allowEmpty = true; public function init()
{
parent::init();
if ($this->message === null) {
$this->message = Yii::t('yii|{attribute} is not a valid URL.');
}
}
/** /**
* Validates the attribute of the object. * Validates the attribute of the object.
@ -47,23 +56,19 @@ class UrlValidator extends Validator
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; $value = $object->$attribute;
if ($this->allowEmpty && $this->isEmpty($value)) { if ($this->validateValue($value)) {
return; if ($this->defaultScheme !== null && strpos($value, '://') === false) {
$object->$attribute = $this->defaultScheme . '://' . $value;
} }
if (($value = $this->validateValue($value)) !== false) {
$object->$attribute = $value;
} else { } else {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); $this->addError($object, $attribute, $this->message);
$this->addError($object, $attribute, $message);
} }
} }
/** /**
* Validates a static value to see if it is a valid URL. * Validates the given value.
* Note that this method does not respect [[allowEmpty]] property. * @param mixed $value the value to be validated.
* This method is provided so that you can call it directly without going through the model validation rule mechanism. * @return boolean whether the value is valid.
* @param mixed $value the value to be validated
* @return mixed false if the the value is not a valid URL, otherwise the possibly modified value ({@see defaultScheme})
*/ */
public function validateValue($value) public function validateValue($value)
{ {
@ -80,7 +85,7 @@ class UrlValidator extends Validator
} }
if (preg_match($pattern, $value)) { if (preg_match($pattern, $value)) {
return $value; return true;
} }
} }
return false; return false;
@ -95,8 +100,7 @@ class UrlValidator extends Validator
*/ */
public function clientValidateAttribute($object, $attribute) public function clientValidateAttribute($object, $attribute)
{ {
$message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); $message = strtr($this->message, array(
$message = strtr($message, array(
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{value}' => $object->$attribute, '{value}' => $object->$attribute,
)); ));
@ -121,7 +125,7 @@ $js
"; ";
} }
if ($this->allowEmpty) { if ($this->skipOnEmpty) {
$js = " $js = "
if($.trim(value)!='') { if($.trim(value)!='') {
$js $js

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

Loading…
Cancel
Save