diff --git a/app/assets/.gitignore b/app/assets/.gitignore
new file mode 100644
index 0000000..72e8ffc
--- /dev/null
+++ b/app/assets/.gitignore
@@ -0,0 +1 @@
+*
diff --git a/app/index.php b/app/index.php
new file mode 100644
index 0000000..8f98090
--- /dev/null
+++ b/app/index.php
@@ -0,0 +1,9 @@
+run();
diff --git a/app/protected/config/main.php b/app/protected/config/main.php
new file mode 100644
index 0000000..e18ead8
--- /dev/null
+++ b/app/protected/config/main.php
@@ -0,0 +1,15 @@
+ 'hello',
+ 'basePath' => dirname(__DIR__),
+ 'components' => array(
+ 'cache' => array(
+ 'class' => 'yii\caching\FileCache',
+ ),
+ 'user' => array(
+ 'class' => 'yii\web\User',
+ 'identityClass' => 'app\models\User',
+ )
+ ),
+);
\ No newline at end of file
diff --git a/app/protected/controllers/SiteController.php b/app/protected/controllers/SiteController.php
new file mode 100644
index 0000000..58e9568
--- /dev/null
+++ b/app/protected/controllers/SiteController.php
@@ -0,0 +1,22 @@
+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'));
+ }
+}
\ No newline at end of file
diff --git a/app/protected/models/User.php b/app/protected/models/User.php
new file mode 100644
index 0000000..cebf1da
--- /dev/null
+++ b/app/protected/models/User.php
@@ -0,0 +1,43 @@
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/app/protected/runtime/.gitignore b/app/protected/runtime/.gitignore
new file mode 100644
index 0000000..72e8ffc
--- /dev/null
+++ b/app/protected/runtime/.gitignore
@@ -0,0 +1 @@
+*
diff --git a/app/protected/views/layouts/main.php b/app/protected/views/layouts/main.php
new file mode 100644
index 0000000..092e665
--- /dev/null
+++ b/app/protected/views/layouts/main.php
@@ -0,0 +1,22 @@
+
+
+
+beginPage(); ?>
+
+ title); ?>
+ head(); ?>
+
+
+
Welcome
+beginBody(); ?>
+
+endBody(); ?>
+
+endPage(); ?>
+
diff --git a/app/protected/views/site/index.php b/app/protected/views/site/index.php
new file mode 100644
index 0000000..3b83080
--- /dev/null
+++ b/app/protected/views/site/index.php
@@ -0,0 +1,17 @@
+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 . " ";
+ echo Html::a('logout', array('logout'));
+}
+?>
+
+
diff --git a/build/.htaccess b/build/.htaccess
new file mode 100644
index 0000000..e019832
--- /dev/null
+++ b/build/.htaccess
@@ -0,0 +1 @@
+deny from all
diff --git a/build/build b/build/build
new file mode 100755
index 0000000..fff4282
--- /dev/null
+++ b/build/build
@@ -0,0 +1,20 @@
+#!/usr/bin/env php
+run();
diff --git a/build/build.bat b/build/build.bat
new file mode 100644
index 0000000..a1ae41f
--- /dev/null
+++ b/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
+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
\ No newline at end of file
diff --git a/build/build.xml b/build/build.xml
new file mode 100644
index 0000000..18a420d
--- /dev/null
+++ b/build/build.xml
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Building package ${pkgname}...
+ Copying files to build directory...
+
+
+
+
+ Changing file permissions...
+
+
+
+
+
+
+
+ Generating source release file...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Building documentation...
+
+ Building Guide PDF...
+
+
+
+
+
+
+ Building Blog PDF...
+
+
+
+
+
+
+ Building API...
+
+
+
+
+ Generating doc release file...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Building online API...
+
+
+
+ Copying tutorials...
+
+
+
+
+
+
+
+ Copying release text files...
+
+
+
+
+
+
+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,
+ ),
+
+
+
+
+
+ Synchronizing code changes for ${pkgname}...
+
+ Building autoload map...
+
+
+ Building yiilite.php...
+
+
+
+
+ Extracting i18n messages...
+
+
+
+
+
+
+ Cleaning up the build...
+
+
+
+
+
+
+ Welcome to use Yii build script!
+ --------------------------------
+ You may use the following command format to build a target:
+
+ phing <target name>
+
+ where <target name> 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
+
+
+
+
diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php
new file mode 100644
index 0000000..d471c0d
--- /dev/null
+++ b/build/controllers/LocaleController.php
@@ -0,0 +1,112 @@
+
+ * @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 "on('add', function($event) {
+$post->on('update', function($event) {
// send email notification
});
~~~
-In the above, we attach an anonymous function to the "add" event of the comment.
-Valid event handlers include:
+In the above, an anonymous function is attached to the "update" event of the post. You may attach
+the following types of event handlers:
- anonymous function: `function($event) { ... }`
- 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.
-You can also attach an event handler to an event when configuring a component with a configuration array. The syntax is
-like the following:
+You can also attach a handler to an event when configuring a component with a configuration array.
+The syntax is like the following:
~~~
array(
@@ -46,15 +48,13 @@ array(
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
-method returns a [[Vector]] object, we can manipulate this object to attach/detach event handlers, or adjust their
-relative orders.
+Sometimes, you may want to associate extra data with an event handler when you attach it to an event
+and then access it when the handler is invoked. You may do so by
~~~
-$handlers = $comment->getEventHandlers('add');
-$handlers->insertAt(0, $callback); // attach a handler as the first one
-$handlers[] = $callback; // attach a handler as the last one
-unset($handlers[0]); // detach the first handler
+$post->on('update', function($event) {
+ // the data can be accessed via $event->data
+}, $data);
~~~
diff --git a/docs/api/base/Object.md b/docs/api/base/Object.md
index a2cea6c..1b9fca0 100644
--- a/docs/api/base/Object.md
+++ b/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,
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
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.
diff --git a/docs/code_style.md b/docs/code_style.md
deleted file mode 100644
index 92a934b..0000000
--- a/docs/code_style.md
+++ /dev/null
@@ -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 ` '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
-
-~~~
-
- * @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)
\ No newline at end of file
diff --git a/docs/full_2011_11_12.png b/docs/full_2011_11_12.png
deleted file mode 100644
index ef50fcb..0000000
Binary files a/docs/full_2011_11_12.png and /dev/null differ
diff --git a/docs/hierarchy_2011_11_12.png b/docs/hierarchy_2011_11_12.png
deleted file mode 100644
index b5f8abc..0000000
Binary files a/docs/hierarchy_2011_11_12.png and /dev/null differ
diff --git a/docs/review_2011_11_12_alex.md b/docs/review_2011_11_12_alex.md
deleted file mode 100644
index 38ffda2..0000000
--- a/docs/review_2011_11_12_alex.md
+++ /dev/null
@@ -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`.
\ No newline at end of file
diff --git a/framework/YiiBase.php b/framework/YiiBase.php
index 16e237d..9d501b1 100644
--- a/framework/YiiBase.php
+++ b/framework/YiiBase.php
@@ -7,6 +7,7 @@
use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
+use yii\base\UnknownClassException;
use yii\logging\Logger;
/**
@@ -46,20 +47,18 @@ class YiiBase
{
/**
* @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.
- * This property mainly affects how [[autoload]] works.
+ * The array keys are the class names (without leading backslashes), and the array values
+ * are the corresponding class file paths (or path aliases). This property mainly affects
+ * how [[autoload()]] works.
* @see import
* @see autoload
*/
public static $classMap = array();
/**
- * @var array list of directories where Yii will search for new classes to be included.
- * The first directory in the array will be searched first, and so on.
- * This property mainly affects how [[autoload]] works.
- * @see import
- * @see autoload
+ * @var boolean whether to search PHP include_path when autoloading unknown classes.
+ * You may want to turn this off if you are also using autoloaders from other libraries.
*/
- public static $classPath = array();
+ public static $enableIncludePath = true;
/**
* @var yii\console\Application|yii\web\Application the application instance
*/
@@ -106,108 +105,115 @@ class YiiBase
}
/**
- * Imports a class or a directory.
- *
- * Importing a class is like including the corresponding class file.
- * The main difference is that importing a class is much lighter because it only
- * includes the class file when the class is referenced in the code the first time.
- *
- * Importing a directory will add the directory to the front of the [[classPath]] array.
- * When [[autoload]] is loading an unknown class, it will search in the directories
- * specified in [[classPath]] to find the corresponding class file to include.
- * For this reason, if multiple directories are imported, the directories imported later
- * will take precedence in class file searching.
- *
- * 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.
- *
- * To import a class or a directory, one can use either path alias or class name (can be namespaced):
- *
- * - `@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
+ * Imports a class by its alias.
+ *
+ * This method is provided to support autoloading of non-namespaced classes.
+ * Such a class can be specified in terms of an alias. For example, the alias `@old/code/Sample`
+ * may represent the `Sample` class under the directory `@old/code` (a path alias).
+ *
+ * By importing a class, the class is put in an internal storage such that when
+ * the class is used for the first time, the class autoloader will be able to
+ * find the corresponding class file and include it. For this reason, this method
+ * is much lighter than `include()`.
+ *
+ * You may import the same class multiple times. Only the first importing will count.
+ *
+ * @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.
+ * @return string the actual class name that `$alias` refers to
+ * @throws Exception if the alias is invalid
*/
- public static function import($alias, $forceInclude = false)
+ public static function import($alias)
{
- if (isset(self::$_imported[$alias])) {
- 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;
- }
+ if (strncmp($alias, '@', 1)) {
return $alias;
+ } else {
+ $alias = static::getAlias($alias);
+ if (!isset(self::$_imported[$alias])) {
+ $className = basename($alias);
+ self::$_imported[$alias] = $className;
+ self::$classMap[$className] = $alias . '.php';
+ }
+ return self::$_imported[$alias];
}
+ }
- $className = basename($alias);
- $isClass = $className !== '*';
-
- if ($isClass && (class_exists($className, false) || interface_exists($className, false))) {
- return self::$_imported[$alias] = $className;
- }
-
- $path = static::getAlias(dirname($alias));
-
- if ($isClass) {
- if ($forceInclude) {
- require($path . "/$className.php");
- self::$_imported[$alias] = $className;
- } else {
- self::$classMap[$className] = $path . DIRECTORY_SEPARATOR . "$className.php";
+ /**
+ * Imports a set of namespaces.
+ *
+ * By importing a namespace, the method will create an alias for the directory corresponding
+ * to the namespace. For example, if "foo\bar" is a namespace associated with the directory
+ * "path/to/foo/bar", then an alias "@foo/bar" will be created for this directory.
+ *
+ * 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.
*
- * The path alias can be either a root alias registered via [[setAlias]] or an
- * 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
- * and the remaining part will be appended to it.
+ * The translation is done according to the following procedure:
+ *
+ * 1. If the given alias does not start with '@', it is returned back without change;
+ * 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'.
+ *
+ * If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config'
+ * would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path.
+ * This is because the longest alias takes precedence.
*
- * In case the given parameter is not an alias (i.e., not starting with '@'),
- * it will be returned back without change.
+ * However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced
+ * instead of '@foo/bar', because '/' serves as the boundary character.
*
- * Note, this method does not ensure the existence of the resulting path.
- * @param string $alias alias
+ * 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.
* 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
*/
public static function getAlias($alias, $throwException = true)
{
- if (is_string($alias)) {
- if (isset(self::$aliases[$alias])) {
- return self::$aliases[$alias];
- } elseif ($alias === '' || $alias[0] !== '@') { // not an alias
- return $alias;
- } elseif (($pos = strpos($alias, '/')) !== false || ($pos = strpos($alias, '\\')) !== false) {
- $rootAlias = substr($alias, 0, $pos);
- if (isset(self::$aliases[$rootAlias])) {
- return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos);
+ if (strncmp($alias, '@', 1)) {
+ // not an alias
+ return $alias;
+ }
+
+ $pos = strpos($alias, '/');
+ $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) {
throw new InvalidParamException("Invalid path alias: $alias");
} 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.
*
- * A path alias is a short name representing a path (a file path, a URL, etc.)
- * A path alias must start with '@' (e.g. '@yii').
+ * A path alias is a short name representing a long path (a file path, a URL, etc.)
+ * For example, we use '@yii' as the alias of the path to the Yii framework directory.
+ *
+ * A path alias must start with the character '@' so that it can be easily differentiated
+ * from non-alias paths.
*
- * Note that this method neither checks the existence of the path nor normalizes the path.
- * Any trailing '/' and '\' characters in the path will be trimmed.
+ * Note that this method does not check if the given path exists or not. All it does is
+ * to associate the alias with the path.
*
- * @param string $alias alias to the path. The alias must start with '@'.
- * @param string $path the path corresponding to the alias. This can be
+ * 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 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
- * actual path first by calling [[getAlias]].
- * @throws Exception if $path is an invalid alias
+ * actual path first by calling [[getAlias()]].
+ *
+ * @throws InvalidParamException if $path is an invalid alias.
* @see getAlias
*/
public static function setAlias($alias, $path)
{
- if ($path === null) {
- unset(self::$aliases[$alias]);
- } elseif ($path[0] !== '@') {
- self::$aliases[$alias] = rtrim($path, '\\/');
- } else {
- self::$aliases[$alias] = static::getAlias($path);
+ if (strncmp($alias, '@', 1)) {
+ $alias = '@' . $alias;
+ }
+ $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 {
+ 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.
- * This method is invoked automatically when the execution encounters an unknown class.
- * The method will attempt to include the class file as follows:
+ * This method is invoked automatically when PHP sees an unknown class.
+ * The method will attempt to include the class file according to the following procedure:
*
* 1. Search in [[classMap]];
* 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`),
* it will attempt to include the file associated with the corresponding path alias
* (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.
*
* @param string $className class name
* @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)
{
- if (isset(self::$classMap[$className])) {
- include(self::$classMap[$className]);
- return true;
- }
+ $className = ltrim($className, '\\');
- if (strpos($className, '\\') !== false) {
- // namespaced class, e.g. yii\base\Component
- // convert namespace to path alias, e.g. yii\base\Component to @yii/base/Component
- $alias = '@' . str_replace('\\', '/', ltrim($className, '\\'));
- if (($path = static::getAlias($alias, false)) !== false) {
- $classFile = $path . '.php';
+ if (isset(self::$classMap[$className])) {
+ $classFile = static::getAlias(self::$classMap[$className]);
+ if (!is_file($classFile)) {
+ throw new InvalidConfigException("Class file does not exist: $classFile");
}
- } elseif (($pos = strpos($className, '_')) !== false) {
- // PEAR-styled class, e.g. PHPUnit_Framework_TestCase
- // convert class name to path alias, e.g. PHPUnit_Framework_TestCase to @PHPUnit/Framework/TestCase
- $alias = '@' . str_replace('_', '/', $className);
- if (($path = static::getAlias($alias, false)) !== false) {
- $classFile = $path . '.php';
+ } else {
+ // follow PSR-0 to determine the class file
+ if (($pos = strrpos($className, '\\')) !== false) {
+ // namespaced class, e.g. yii\base\Component
+ $path = str_replace('\\', '/', substr($className, 0, $pos + 1))
+ . str_replace('_', '/', substr($className, $pos + 1)) . '.php';
+ } else {
+ $path = str_replace('_', '/', $className) . '.php';
}
- }
- if (!isset($classFile)) {
- // search in include paths
- foreach (self::$classPath as $path) {
- $path .= DIRECTORY_SEPARATOR . $className . '.php';
- if (is_file($path)) {
- $classFile = $path;
- $alias = $className;
+ // try via path alias first
+ if (strpos($path, '/') !== false) {
+ $fullPath = static::getAlias('@' . $path, false);
+ if ($fullPath !== false && is_file($fullPath)) {
+ $classFile = $fullPath;
}
}
- }
- if (isset($classFile, $alias) && is_file($classFile)) {
- if (!YII_DEBUG || basename(realpath($classFile)) === basename($alias) . '.php') {
- include($classFile);
- return true;
- } else {
- throw new Exception("Class name '$className' does not match the class file '" . realpath($classFile) . "'. Have you checked their case sensitivity?");
+ // search include_path
+ if (!isset($classFile) && self::$enableIncludePath && ($fullPath = stream_resolve_include_path($path)) !== false) {
+ $classFile = $fullPath;
+ }
+
+ if (!isset($classFile)) {
+ // return false to let other autoloaders to try loading the class
+ return false;
}
}
- return false;
+ include($classFile);
+
+ if (class_exists($className, false) || interface_exists($className, false)) {
+ return true;
+ } else {
+ throw new UnknownClassException("Unable to find '$className' in file: $classFile");
+ }
}
/**
* Creates a new object using the given configuration.
*
* The configuration can be either a string or an array.
- * If a string, it is treated as the *object type*; if an array,
- * it must contain a `class` element specifying the *object type*, and
+ * If a string, it is treated as the *object class*; if an array,
+ * 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 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,
*
- * - `\app\components\GoogleMap`: fully-qualified namespaced class.
- * - `@app/components/GoogleMap`: an alias
+ * - `app\components\GoogleMap`: fully-qualified namespaced class.
+ * - `@app/components/GoogleMap`: an alias, used for non-namespaced class.
*
* Below are some usage examples:
*
@@ -366,7 +431,13 @@ class YiiBase
}
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) {
@@ -504,23 +575,31 @@ class YiiBase
/**
* 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
- * number value. This feature is mainly used to solve plural format issue in case
- * a message has different plural forms in some languages.
- * @param string $message the original message
- * @param array $params parameters to be applied to the message using strtr.
- * The first parameter can be a number without key.
- * And in this case, the method will call {@link CChoiceFormat::format} to choose
- * an appropriate message translation.
- * You can pass parameter for {@link CChoiceFormat::format}
- * or plural forms format without wrapping it with array.
- * @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used.
- * @return string the translated message
- * @see CMessageSource
+ *
+ * The translation will be conducted according to the message category and the target language.
+ * To specify the category of the message, prefix the message with the category name and separate it
+ * with "|". For example, "app|hello world". If the category is not specified, the default category "app"
+ * will be used. The actual message translation is done by a [[\yii\i18n\MessageSource|message source]].
+ *
+ * In case when a translated message has different plural forms (separated by "|"), this method
+ * will also attempt to choose an appropriate one according to a given numeric value which is
+ * specified as the first parameter (indexed by 0) in `$params`.
+ *
+ * For example, if a translated message is "I have an apple.|I have {n} apples.", and the first
+ * parameter is 2, the message returned will be "I have 2 apples.". Note that the placeholder "{n}"
+ * will be replaced with the given number.
+ *
+ * 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)
{
- return Yii::$app->getI18N()->translate($message, $params, $language);
+ return self::$app->getI18N()->translate($message, $params, $language);
}
}
diff --git a/framework/assets.php b/framework/assets.php
new file mode 100644
index 0000000..5efa94a
--- /dev/null
+++ b/framework/assets.php
@@ -0,0 +1,5 @@
+ __DIR__ . '/web/assets',
+);
\ No newline at end of file
diff --git a/framework/base/ActionEvent.php b/framework/base/ActionEvent.php
index 7c5a40c..9507b12 100644
--- a/framework/base/ActionEvent.php
+++ b/framework/base/ActionEvent.php
@@ -22,7 +22,9 @@ class ActionEvent extends Event
*/
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;
diff --git a/framework/base/Application.php b/framework/base/Application.php
index 9be1939..c498a8e 100644
--- a/framework/base/Application.php
+++ b/framework/base/Application.php
@@ -13,36 +13,6 @@ use yii\helpers\FileHelper;
/**
* 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:
- *
- *
{@link getErrorHandler errorHandler}: handles PHP errors and
- * uncaught exceptions. This application component is dynamically loaded when needed.
- *
{@link getSecurityManager securityManager}: provides security-related
- * services, such as hashing, encryption. This application component is dynamically
- * loaded when needed.
- *
{@link getStatePersister statePersister}: provides global state
- * persistence method. This application component is dynamically loaded when needed.
- *
{@link getCache cache}: provides caching feature. This application component is
- * disabled by default.
- *
- *
- * Application will undergo the following life cycles when processing a user request:
- *
- *
load application configuration;
- *
set up class autoloader and error handling;
- *
load static application components;
- *
{@link beforeRequest}: preprocess the user request; `beforeRequest` event raised.
- *
{@link processRequest}: process the user request;
- *
{@link afterRequest}: postprocess the user request; `afterRequest` event raised.
- *
- *
- * 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
* @since 2.0
*/
@@ -87,50 +57,51 @@ class Application extends Module
*/
public $layout = 'main';
- // todo
- public $localeDataPath = '@yii/i18n/data';
-
- private $_runtimePath;
private $_ended = false;
/**
- * @var string Used to reserve memory for fatal error handler. This memory
- * reserve can be removed if it's OK to write to PHP log only in this particular case.
+ * @var string Used to reserve memory for fatal error handler.
*/
private $_memoryReserve;
/**
* Constructor.
- * @param string $id the ID of this application. The ID should uniquely identify the application from others.
- * @param string $basePath the base path of this application. This should point to
- * the directory containing all application logic, template and data.
- * @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.
+ * Note that the configuration must contain both [[id]] and [[basePath]].
+ * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing.
*/
- public function __construct($id, $basePath, $config = array())
+ public function __construct($config = array())
{
Yii::$app = $this;
- $this->id = $id;
- $this->setBasePath($basePath);
- if (YII_ENABLE_ERROR_HANDLER) {
- ini_set('display_errors', 0);
- set_exception_handler(array($this, 'handleException'));
- set_error_handler(array($this, 'handleError'), error_reporting());
+ if (!isset($config['id'])) {
+ throw new InvalidConfigException('The "id" configuration is required.');
}
- $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();
Component::__construct($config);
}
/**
- * Initializes the application by loading components declared in [[preload]].
- * If you override this method, make sure the parent implementation is invoked.
+ * Registers error handlers.
*/
- 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.
* This is the main entrance of an application.
* @return integer the exit status (0 means normal, non-zero values mean abnormal)
@@ -246,6 +168,8 @@ class Application extends Module
return 0;
}
+ private $_runtimePath;
+
/**
* Returns the directory that stores runtime files.
* @return string the directory that stores runtime files. Defaults to 'protected/runtime'.
@@ -265,12 +189,35 @@ class Application extends Module
*/
public function setRuntimePath($path)
{
- $p = FileHelper::ensureDirectory($path);
- if (is_writable($p)) {
- $this->_runtimePath = $p;
+ $path = Yii::getAlias($path);
+ if (is_dir($path) && is_writable($path)) {
+ $this->_runtimePath = $path;
} 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);
}
/**
@@ -295,37 +242,6 @@ class Application extends Module
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.
* @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.
* @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.
*
* This method is used as a PHP error handler. It will simply raise an `ErrorException`.
@@ -450,41 +397,27 @@ 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
+ * Handles fatal PHP errors
*/
- public function handleException($exception)
+ public function handleFatalError()
{
- // disable error capturing to avoid recursive errors while handling exceptions
- restore_error_handler();
- restore_exception_handler();
-
- try {
- $this->logException($exception);
+ if (YII_ENABLE_ERROR_HANDLER) {
+ $error = error_get_last();
- if (($handler = $this->getErrorHandler()) !== null) {
- $handler->handle($exception);
- } else {
- $this->renderException($exception);
- }
+ if (ErrorException::isFatalError($error)) {
+ 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);
- $this->end(1);
+ if (($handler = $this->getErrorHandler()) !== null) {
+ $handler->handle($exception);
+ } else {
+ $this->renderException($exception);
+ }
- } 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;
+ exit(1);
}
- $msg .= "\n\$_SERVER = " . var_export($_SERVER, true);
- error_log($msg);
- exit(1);
}
}
diff --git a/framework/base/Component.php b/framework/base/Component.php
index f1d549b..80259e7 100644
--- a/framework/base/Component.php
+++ b/framework/base/Component.php
@@ -7,26 +7,23 @@
namespace yii\base;
+use Yii;
+
/**
- * Component is the base class that provides the *property*, *event* and *behavior* features.
- *
* @include @yii/base/Component.md
- *
- * @property Behavior[] behaviors list of behaviors currently attached to this component
- *
* @author Qiang Xue
* @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)
*/
- private $_b;
+ private $_behaviors;
/**
* Returns the value of a component property.
@@ -52,7 +49,7 @@ class Component extends \yii\base\Object
} else {
// behavior property
$this->ensureBehaviors();
- foreach ($this->_b as $behavior) {
+ foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name;
}
@@ -87,17 +84,16 @@ class Component extends \yii\base\Object
return;
} elseif (strncmp($name, 'on ', 3) === 0) {
// on event: attach event handler
- $name = trim(substr($name, 3));
- $this->getEventHandlers($name)->add($value);
+ $this->on(trim(substr($name, 3)), $value);
return;
} elseif (strncmp($name, 'as ', 3) === 0) {
// as behavior: attach behavior
$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 {
// behavior property
$this->ensureBehaviors();
- foreach ($this->_b as $behavior) {
+ foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) {
$behavior->$name = $value;
return;
@@ -131,7 +127,7 @@ class Component extends \yii\base\Object
} else {
// behavior property
$this->ensureBehaviors();
- foreach ($this->_b as $behavior) {
+ foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name !== null;
}
@@ -161,7 +157,7 @@ class Component extends \yii\base\Object
} else {
// behavior property
$this->ensureBehaviors();
- foreach ($this->_b as $behavior) {
+ foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) {
$behavior->$name = null;
return;
@@ -198,7 +194,7 @@ class Component extends \yii\base\Object
}
$this->ensureBehaviors();
- foreach ($this->_b as $object) {
+ foreach ($this->_behaviors as $object) {
if (method_exists($object, $name)) {
return call_user_func_array(array($object, $name), $params);
}
@@ -213,8 +209,8 @@ class Component extends \yii\base\Object
*/
public function __clone()
{
- $this->_e = null;
- $this->_b = null;
+ $this->_events = null;
+ $this->_behaviors = null;
}
/**
@@ -259,7 +255,7 @@ class Component extends \yii\base\Object
return true;
} else {
$this->ensureBehaviors();
- foreach ($this->_b as $behavior) {
+ foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name, $checkVar)) {
return true;
}
@@ -289,7 +285,7 @@ class Component extends \yii\base\Object
return true;
} else {
$this->ensureBehaviors();
- foreach ($this->_b as $behavior) {
+ foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name, $checkVar)) {
return true;
}
@@ -337,44 +333,17 @@ class Component extends \yii\base\Object
public function hasEventHandlers($name)
{
$this->ensureBehaviors();
- return isset($this->_e[$name]) && $this->_e[$name]->getCount();
- }
-
- /**
- * 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];
+ return !empty($this->_events[$name]);
}
/**
* 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
* some examples:
*
* ~~~
- * function($event) { ... } // anonymous function
+ * function ($event) { ... } // anonymous function
* array($object, 'handleClick') // $object->handleClick()
* array('Page', 'handleClick') // Page::handleClick()
* 'handleClick' // global function handleClick()
@@ -383,31 +352,53 @@ class Component extends \yii\base\Object
* 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.
*
* @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()
*/
- 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.
* This method is the opposite of [[on()]].
* @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
* @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)
{
$this->ensureBehaviors();
- if (isset($this->_e[$name]) && $this->_e[$name]->getCount()) {
+ if (!empty($this->_events[$name])) {
if ($event === null) {
$event = new Event;
}
@@ -429,8 +420,9 @@ class Component extends \yii\base\Object
}
$event->handled = false;
$event->name = $name;
- foreach ($this->_e[$name] as $handler) {
- call_user_func($handler, $event);
+ foreach ($this->_events[$name] as $handler) {
+ $event->data = $handler[1];
+ call_user_func($handler[0], $event);
// stop further handling if the event is handled
if ($event instanceof Event && $event->handled) {
return;
@@ -447,7 +439,7 @@ class Component extends \yii\base\Object
public function getBehavior($name)
{
$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()
{
$this->ensureBehaviors();
- return $this->_b;
+ return $this->_behaviors;
}
/**
* Attaches a behavior to this component.
* This method will create the behavior object based on the given
* 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|array|Behavior $behavior the behavior configuration. This can be one of the following:
*
* - a [[Behavior]] object
* - 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
* @see detachBehavior
@@ -498,15 +490,15 @@ class Component extends \yii\base\Object
/**
* 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.
* @return Behavior the detached behavior. Null if the behavior does not exist.
*/
public function detachBehavior($name)
{
- if (isset($this->_b[$name])) {
- $behavior = $this->_b[$name];
- unset($this->_b[$name]);
+ if (isset($this->_behaviors[$name])) {
+ $behavior = $this->_behaviors[$name];
+ unset($this->_behaviors[$name]);
$behavior->detach();
return $behavior;
} else {
@@ -519,12 +511,12 @@ class Component extends \yii\base\Object
*/
public function detachBehaviors()
{
- if ($this->_b !== null) {
- foreach ($this->_b as $name => $behavior) {
+ if ($this->_behaviors !== null) {
+ foreach ($this->_behaviors as $name => $behavior) {
$this->detachBehavior($name);
}
}
- $this->_b = array();
+ $this->_behaviors = array();
}
/**
@@ -532,8 +524,8 @@ class Component extends \yii\base\Object
*/
public function ensureBehaviors()
{
- if ($this->_b === null) {
- $this->_b = array();
+ if ($this->_behaviors === null) {
+ $this->_behaviors = array();
foreach ($this->behaviors() as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior);
}
@@ -549,12 +541,12 @@ class Component extends \yii\base\Object
private function attachBehaviorInternal($name, $behavior)
{
if (!($behavior instanceof Behavior)) {
- $behavior = \Yii::createObject($behavior);
+ $behavior = Yii::createObject($behavior);
}
- if (isset($this->_b[$name])) {
- $this->_b[$name]->detach();
+ if (isset($this->_behaviors[$name])) {
+ $this->_behaviors[$name]->detach();
}
$behavior->attach($this);
- return $this->_b[$name] = $behavior;
+ return $this->_behaviors[$name] = $behavior;
}
}
diff --git a/framework/base/Controller.php b/framework/base/Controller.php
index ff6d8f7..d11d19b 100644
--- a/framework/base/Controller.php
+++ b/framework/base/Controller.php
@@ -47,6 +47,11 @@ class Controller extends Component
* by [[run()]] when it is called by [[Application]] to run an 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
@@ -135,7 +140,7 @@ class Controller extends Component
} elseif ($pos > 0) {
return $this->module->runAction($route, $params);
} 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.
+ *
+ * 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 array $params the parameters (name-value pairs) that should be made available in the view.
* These parameters will not be available in the layout.
@@ -301,10 +337,11 @@ class Controller extends Component
*/
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();
if ($layoutFile !== false) {
- return Yii::$app->getView()->renderFile($layoutFile, array('content' => $output), $this);
+ return $this->getView()->renderFile($layoutFile, array('content' => $output), $this);
} else {
return $output;
}
@@ -313,14 +350,15 @@ class Controller extends Component
/**
* Renders a view.
* 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.
* @return string the rendering result.
* @throws InvalidParamException if the view file does not exist.
*/
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())
{
- 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.
- *
- * 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.
+ * Please refer to [[render()]] on how to specify this parameter.
* @throws InvalidParamException if an invalid path alias is used to specify the layout
*/
protected function findLayoutFile()
@@ -399,7 +463,7 @@ class Controller extends Component
$file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view;
}
- if (FileHelper::getExtension($file) === '') {
+ if (pathinfo($file, PATHINFO_EXTENSION) === '') {
$file .= '.php';
}
return $file;
diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php
index 465d839..b41e9ed 100644
--- a/framework/base/ErrorException.php
+++ b/framework/base/ErrorException.php
@@ -7,6 +7,8 @@
namespace yii\base;
+use Yii;
+
/**
* ErrorException represents a PHP error.
*
@@ -33,6 +35,32 @@ class ErrorException extends Exception
$this->severity = $severity;
$this->file = $filename;
$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()
* @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));
}
@@ -62,20 +90,20 @@ class ErrorException extends Exception
public function getName()
{
$names = array(
- E_ERROR => \Yii::t('yii|Fatal Error'),
- E_PARSE => \Yii::t('yii|Parse Error'),
- E_CORE_ERROR => \Yii::t('yii|Core Error'),
- E_COMPILE_ERROR => \Yii::t('yii|Compile Error'),
- E_USER_ERROR => \Yii::t('yii|User Error'),
- E_WARNING => \Yii::t('yii|Warning'),
- E_CORE_WARNING => \Yii::t('yii|Core Warning'),
- E_COMPILE_WARNING => \Yii::t('yii|Compile Warning'),
- E_USER_WARNING => \Yii::t('yii|User Warning'),
- E_STRICT => \Yii::t('yii|Strict'),
- E_NOTICE => \Yii::t('yii|Notice'),
- E_RECOVERABLE_ERROR => \Yii::t('yii|Recoverable Error'),
- E_DEPRECATED => \Yii::t('yii|Deprecated'),
+ E_ERROR => Yii::t('yii|Fatal Error'),
+ E_PARSE => Yii::t('yii|Parse Error'),
+ E_CORE_ERROR => Yii::t('yii|Core Error'),
+ E_COMPILE_ERROR => Yii::t('yii|Compile Error'),
+ E_USER_ERROR => Yii::t('yii|User Error'),
+ E_WARNING => Yii::t('yii|Warning'),
+ E_CORE_WARNING => Yii::t('yii|Core Warning'),
+ E_COMPILE_WARNING => Yii::t('yii|Compile Warning'),
+ E_USER_WARNING => Yii::t('yii|User Warning'),
+ E_STRICT => Yii::t('yii|Strict'),
+ E_NOTICE => Yii::t('yii|Notice'),
+ E_RECOVERABLE_ERROR => Yii::t('yii|Recoverable Error'),
+ 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');
}
}
diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php
index f71b8c8..98a061d 100644
--- a/framework/base/ErrorHandler.php
+++ b/framework/base/ErrorHandler.php
@@ -16,8 +16,6 @@ namespace yii\base;
* @author Qiang Xue
* @since 2.0
*/
-use yii\helpers\VarDumper;
-
class ErrorHandler extends Component
{
/**
@@ -53,6 +51,7 @@ class ErrorHandler extends Component
/**
+ * Handles exception
* @param \Exception $exception
*/
public function handle($exception)
@@ -63,10 +62,14 @@ class ErrorHandler extends Component
$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) {
\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') {
\Yii::$app->renderException($exception);
} 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;
if (!YII_DEBUG || $exception instanceof UserException) {
$viewName = $this->errorView;
} else {
$viewName = $this->exceptionView;
}
- echo $view->render($viewName, array(
+ echo $view->renderFile($viewName, array(
'exception' => $exception,
), $this);
}
@@ -198,6 +207,10 @@ class ErrorHandler extends Component
echo '
' . $output . '
';
}
+ /**
+ * Renders calls stack trace
+ * @param array $trace
+ */
public function renderTrace($trace)
{
$count = 0;
@@ -235,6 +248,11 @@ class ErrorHandler extends Component
echo '';
}
+ /**
+ * Converts special characters to HTML entities
+ * @param string $text text to encode
+ * @return string
+ */
public function htmlEncode($text)
{
return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset);
@@ -255,7 +273,7 @@ class ErrorHandler extends Component
{
$view = new View;
$name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView;
- echo $view->render($name, array(
+ echo $view->renderFile($name, array(
'exception' => $exception,
), $this);
}
diff --git a/framework/base/Event.php b/framework/base/Event.php
index b86ed7c..5d40736 100644
--- a/framework/base/Event.php
+++ b/framework/base/Event.php
@@ -15,12 +15,14 @@ namespace yii\base;
* And the [[handled]] property indicates if the event is handled.
* If an event handler sets [[handled]] to be true, the rest of the
* 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
* @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()]].
@@ -39,7 +41,8 @@ class Event extends \yii\base\Object
*/
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;
}
diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php
index 94a9a55..948d96b 100644
--- a/framework/base/HttpException.php
+++ b/framework/base/HttpException.php
@@ -29,11 +29,12 @@ class HttpException extends UserException
* @param integer $status HTTP status code, such as 404, 500, etc.
* @param string $message error message
* @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;
- parent::__construct($message, $code);
+ parent::__construct($message, $code, $previous);
}
/**
diff --git a/framework/base/Model.php b/framework/base/Model.php
index 13e567d..7f55239 100644
--- a/framework/base/Model.php
+++ b/framework/base/Model.php
@@ -8,8 +8,8 @@
namespace yii\base;
use yii\helpers\StringHelper;
-use yii\validators\Validator;
use yii\validators\RequiredValidator;
+use yii\validators\Validator;
/**
* 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.
* By default, this method returns all public non-static properties of the class.
* 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)
{
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__);
}
}
diff --git a/framework/base/Module.php b/framework/base/Module.php
index 6b82157..d99778d 100644
--- a/framework/base/Module.php
+++ b/framework/base/Module.php
@@ -170,7 +170,6 @@ abstract class Module extends Component
*/
public function init()
{
- Yii::setAlias('@' . $this->id, $this->getBasePath());
$this->preloadComponents();
}
@@ -208,11 +207,17 @@ abstract class Module extends Component
* Sets the root directory of the module.
* 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.
- * @throws Exception if the directory does not exist.
+ * @throws InvalidParamException if the directory does not exist.
*/
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)
{
- $this->_controllerPath = FileHelper::ensureDirectory($path);
+ $this->_controllerPath = Yii::getAlias($path);
}
/**
@@ -260,7 +265,7 @@ abstract class Module extends Component
*/
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)
{
- $this->_layoutPath = FileHelper::ensureDirectory($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);
- }
+ $this->_layoutPath = Yii::getAlias($path);
}
/**
@@ -346,7 +338,7 @@ abstract class Module extends Component
if ($this->_modules[$id] instanceof Module) {
return $this->_modules[$id];
} 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);
}
}
@@ -452,7 +444,7 @@ abstract class Module extends Component
if ($this->_components[$id] instanceof Component) {
return $this->_components[$id];
} elseif ($load) {
- Yii::trace("Loading component: $id", __CLASS__);
+ Yii::trace("Loading component: $id", __METHOD__);
return $this->_components[$id] = Yii::createObject($this->_components[$id]);
}
}
@@ -580,8 +572,9 @@ abstract class Module extends Component
* instance of it.
*
* @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
- * with the remainder of the route which represents the action ID. Otherwise false will be returned.
+ * @return array|boolean If the controller is created successfully, it will be returned together
+ * 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)
{
@@ -605,16 +598,16 @@ abstract class Module extends Component
$controller = Yii::createObject($this->controllerMap[$id], $id, $this);
} elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) {
$className = StringHelper::id2camel($id) . 'Controller';
-
$classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php';
- if (is_file($classFile)) {
- $className = $this->controllerNamespace . '\\' . $className;
- if (!class_exists($className, false)) {
- require($classFile);
- }
- if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) {
- $controller = new $className($id, $this);
- }
+ if (!is_file($classFile)) {
+ return false;
+ }
+ $className = ltrim($this->controllerNamespace . '\\' . $className, '\\');
+ Yii::$classMap[$className] = $classFile;
+ if (is_subclass_of($className, 'yii\base\Controller')) {
+ $controller = new $className($id, $this);
+ } elseif (YII_DEBUG) {
+ throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller.");
}
}
diff --git a/framework/base/Object.php b/framework/base/Object.php
index 3bd8378..a547990 100644
--- a/framework/base/Object.php
+++ b/framework/base/Object.php
@@ -8,10 +8,7 @@
namespace yii\base;
/**
- * Object is the base class that provides the *property* feature.
- *
* @include @yii/base/Object.md
- *
* @author Qiang Xue
* @since 2.0
*/
diff --git a/framework/base/Response.php b/framework/base/Response.php
index a53fd61..396b073 100644
--- a/framework/base/Response.php
+++ b/framework/base/Response.php
@@ -13,28 +13,38 @@ namespace yii\base;
*/
class Response extends Component
{
+ /**
+ * Starts output buffering
+ */
public function beginOutput()
{
ob_start();
ob_implicit_flush(false);
}
+ /**
+ * Returns contents of the output buffer and discards it
+ * @return string output buffer contents
+ */
public function endOutput()
{
return ob_get_clean();
}
+ /**
+ * Returns contents of the output buffer
+ * @return string output buffer contents
+ */
public function getOutput()
{
return ob_get_contents();
}
- public function cleanOutput()
- {
- ob_clean();
- }
-
- public function removeOutput($all = true)
+ /**
+ * Discards the output buffer
+ * @param boolean $all if true recursively discards all output buffers used
+ */
+ public function cleanOutput($all = true)
{
if ($all) {
for ($level = ob_get_level(); $level > 0; --$level) {
diff --git a/framework/base/Theme.php b/framework/base/Theme.php
index 88ecb0a..a60d56e 100644
--- a/framework/base/Theme.php
+++ b/framework/base/Theme.php
@@ -33,11 +33,17 @@ use yii\helpers\FileHelper;
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
*/
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.
* 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.
@@ -45,7 +51,6 @@ class Theme extends Component
*/
public $pathMap;
- private $_baseUrl;
/**
* Initializes the theme.
@@ -56,10 +61,10 @@ class Theme extends Component
parent::init();
if (empty($this->pathMap)) {
if ($this->basePath !== null) {
- $this->basePath = FileHelper::ensureDirectory($this->basePath);
+ $this->basePath = Yii::getAlias($this->basePath);
$this->pathMap = array(Yii::$app->getBasePath() => $this->basePath);
} else {
- throw new InvalidConfigException("Theme::basePath must be set.");
+ throw new InvalidConfigException('The "basePath" property must be set.');
}
}
$paths = array();
@@ -69,25 +74,11 @@ class Theme extends Component
$paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR;
}
$this->pathMap = $paths;
- }
-
- /**
- * 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), '/');
+ if ($this->baseUrl === null) {
+ throw new InvalidConfigException('The "baseUrl" property must be set.');
+ } else {
+ $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/');
+ }
}
/**
@@ -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.
* @return string the absolute URL
*/
diff --git a/framework/base/UnknownClassException.php b/framework/base/UnknownClassException.php
new file mode 100644
index 0000000..ac44746
--- /dev/null
+++ b/framework/base/UnknownClassException.php
@@ -0,0 +1,26 @@
+
+ * @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');
+ }
+}
+
diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php
index 29bedca..440e76e 100644
--- a/framework/base/UnknownMethodException.php
+++ b/framework/base/UnknownMethodException.php
@@ -8,7 +8,7 @@
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
* @since 2.0
diff --git a/framework/base/View.php b/framework/base/View.php
index c7087c1..10a7053 100644
--- a/framework/base/View.php
+++ b/framework/base/View.php
@@ -10,6 +10,7 @@ namespace yii\base;
use Yii;
use yii\base\Application;
use yii\helpers\FileHelper;
+use yii\helpers\Html;
/**
* View represents a view object in the MVC pattern.
@@ -22,7 +23,46 @@ use yii\helpers\FileHelper;
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 = '';
+ /**
+ * This is internally used as the placeholder for receiving the content registered for the beginning of the body section.
+ */
+ const PL_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 = '';
+
+
+ /**
+ * @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;
/**
@@ -35,31 +75,75 @@ class View extends Component
*/
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.
*/
public $theme;
/**
- * @var array a list of named output clips. You can call [[beginClip()]] and [[endClip()]]
- * to capture small fragments of a view. They can be later accessed at somewhere else
+ * @var array a list of named output blocks. The keys are the block names and the values
+ * 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.
*/
- public $clips;
+ public $blocks;
/**
* @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.
+ * @internal
*/
public $widgetStack = array();
/**
* @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();
/**
* @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();
+ /**
+ * @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.
*
- * This method will call [[findViewFile()]] to convert the view name into the corresponding view
- * file path, and it will then call [[renderFile()]] to render the view.
+ * This method delegates the call to the [[context]] object:
+ *
+ * - 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 [[findViewFile()]] on how to specify this parameter.
+ * @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 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
+ * @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.
* @see renderFile
- * @see findViewFile
*/
- public function render($view, $params = array(), $context = null)
+ public function render($view, $params = array())
{
- $viewFile = $this->findViewFile($context, $view);
- return $this->renderFile($viewFile, $params, $context);
+ if ($this->context instanceof Controller) {
+ 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,10 +224,14 @@ class View extends Component
$this->context = $context;
}
- if ($this->renderer !== null) {
- $output = $this->renderer->render($this, $viewFile, $params);
- } else {
- $output = $this->renderPhpFile($viewFile, $params);
+ $output = '';
+ if ($this->beforeRender($viewFile)) {
+ if ($this->renderer !== null) {
+ $output = $this->renderer->render($this, $viewFile, $params);
+ } else {
+ $output = $this->renderPhpFile($viewFile, $params);
+ }
+ $this->afterRender($viewFile, $output);
}
$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.
*
* 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)) {
$n = count($this->dynamicPlaceholders);
- $placeholder = "";
+ $placeholder = "";
$this->addDynamicPlaceholder($placeholder, $statements);
return $placeholder;
} 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.
* This method will use [[Yii::createObject()]] to create the widget.
* @param string $class the widget class name or path alias
@@ -265,7 +349,10 @@ class View extends Component
public function createWidget($class, $properties = array())
{
$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.
- * This method is a shortcut to beginning [[yii\widgets\Clip]]
- * @param string $id the clip ID.
- * @param boolean $renderInPlace whether to render the clip content in place.
- * Defaults to false, meaning the captured clip will not be displayed.
- * @return \yii\widgets\Clip the Clip widget instance
- * @see \yii\widgets\Clip
+ * Begins recording a block.
+ * This method is a shortcut to beginning [[yii\widgets\Block]]
+ * @param string $id the block ID.
+ * @param boolean $renderInPlace whether to render the block content in place.
+ * Defaults to false, meaning the captured block will not be displayed.
+ * @return \yii\widgets\Block the Block widget instance
*/
- 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,
'renderInPlace' => $renderInPlace,
- 'view' => $this,
));
}
/**
- * Ends recording a clip.
+ * Ends recording a block.
*/
- public function endClip()
+ public function endBlock()
{
$this->endWidget();
}
/**
* 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.
- * Please refer to [[View::findViewFile()]] on how to set this property.
+ * This method can be used to implement nested layout. For example, a layout can be embedded
+ * in another layout file specified as '@app/view/layouts/base' like the following:
+ *
+ * ~~~
+ * beginContent('@app/view/layouts/base'); ?>
+ * ...layout content here...
+ * 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.
* @return \yii\widgets\ContentDecorator the ContentDecorator widget instance
* @see \yii\widgets\ContentDecorator
*/
- public function beginContent($view, $params = array())
+ public function beginContent($viewFile, $params = array())
{
return $this->beginWidget('yii\widgets\ContentDecorator', array(
- 'view' => $this,
- 'viewName' => $view,
+ 'viewFile' => $viewFile,
'params' => $params,
));
}
@@ -400,7 +493,6 @@ class View extends Component
public function beginCache($id, $properties = array())
{
$properties['id'] = $id;
- $properties['view'] = $this;
/** @var $cache \yii\widgets\FragmentCache */
$cache = $this->beginWidget('yii\widgets\FragmentCache', $properties);
if ($cache->getCachedContent() !== false) {
@@ -418,4 +510,273 @@ class View extends Component
{
$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);
+ }
}
\ No newline at end of file
diff --git a/framework/base/ViewEvent.php b/framework/base/ViewEvent.php
new file mode 100644
index 0000000..cac7be4
--- /dev/null
+++ b/framework/base/ViewEvent.php
@@ -0,0 +1,44 @@
+
+ * @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);
+ }
+}
\ No newline at end of file
diff --git a/framework/base/Widget.php b/framework/base/Widget.php
index 24d0685..13e6d30 100644
--- a/framework/base/Widget.php
+++ b/framework/base/Widget.php
@@ -19,9 +19,11 @@ use yii\helpers\FileHelper;
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.
*/
@@ -32,17 +34,6 @@ class Widget extends Component
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.
* @param boolean $autoGenerate whether to generate an ID if it is not set previously
* @return string ID of the widget.
@@ -73,6 +64,18 @@ class Widget extends Component
/**
* 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 array $params the parameters (name-value pairs) that should be made available in the view.
* @return string the rendering result.
@@ -80,7 +83,7 @@ class Widget extends Component
*/
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())
{
- 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);
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;
+ }
}
\ No newline at end of file
diff --git a/framework/caching/ApcCache.php b/framework/caching/ApcCache.php
index dd954cc..391851d 100644
--- a/framework/caching/ApcCache.php
+++ b/framework/caching/ApcCache.php
@@ -11,6 +11,7 @@ namespace yii\caching;
* 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.
+ * 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.
*
diff --git a/framework/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php
index 9c4e547..7c7058e 100644
--- a/framework/caching/ChainedDependency.php
+++ b/framework/caching/ChainedDependency.php
@@ -22,11 +22,10 @@ namespace yii\caching;
class ChainedDependency extends Dependency
{
/**
- * @var array list of dependencies that this dependency is composed of.
- * Each array element should be a dependency object or a configuration array
- * that can be used to create a dependency object via [[\Yii::createObject()]].
+ * @var Dependency[] list of dependencies that this dependency is composed of.
+ * Each array element must be a dependency object.
*/
- public $dependencies = array();
+ public $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.
@@ -37,9 +36,8 @@ class ChainedDependency extends Dependency
/**
* Constructor.
- * @param array $dependencies list of dependencies that this dependency is composed of.
- * Each array element should be a dependency object or a configuration array
- * that can be used to create a dependency object via [[\Yii::createObject()]].
+ * @param Dependency[] $dependencies list of dependencies that this dependency is composed of.
+ * Each array element should be a dependency object.
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($dependencies = array(), $config = array())
@@ -54,10 +52,7 @@ class ChainedDependency extends Dependency
public function evaluateDependency()
{
foreach ($this->dependencies as $dependency) {
- if (!$dependency instanceof Dependency) {
- $dependency = \Yii::createObject($dependency);
- }
- $dependency->evalulateDependency();
+ $dependency->evaluateDependency();
}
}
@@ -79,10 +74,7 @@ class ChainedDependency extends Dependency
*/
public function getHasChanged()
{
- foreach ($this->dependencies as $i => $dependency) {
- if (!$dependency instanceof Dependency) {
- $this->dependencies[$i] = $dependency = \Yii::createObject($dependency);
- }
+ foreach ($this->dependencies as $dependency) {
if ($this->dependOnAll && $dependency->getHasChanged()) {
return true;
} elseif (!$this->dependOnAll && !$dependency->getHasChanged()) {
diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php
index 3952852..dee8c7a 100644
--- a/framework/caching/DbCache.php
+++ b/framework/caching/DbCache.php
@@ -99,7 +99,7 @@ class DbCache extends Cache
$query = new Query;
$query->select(array('data'))
->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) {
// temporarily disable and re-enable query caching
$this->db->enableQueryCache = false;
@@ -125,7 +125,7 @@ class DbCache extends Cache
$query->select(array('id', 'data'))
->from($this->cacheTable)
->where(array('id' => $keys))
- ->andWhere('(expire = 0 OR expire > ' . time() . ')');
+ ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')');
if ($this->db->enableQueryCache) {
$this->db->enableQueryCache = false;
@@ -227,7 +227,7 @@ class DbCache extends Cache
{
if ($force || mt_rand(0, 1000000) < $this->gcProbability) {
$this->db->createCommand()
- ->delete($this->cacheTable, 'expire > 0 AND expire < ' . time())
+ ->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time())
->execute();
}
}
diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php
index cbe0ae1..7d45223 100644
--- a/framework/caching/DbDependency.php
+++ b/framework/caching/DbDependency.php
@@ -52,6 +52,7 @@ class DbDependency extends Dependency
/**
* Generates the data needed to determine if dependency has been changed.
* This method returns the value of the global state.
+ * @throws InvalidConfigException
* @return mixed the data needed to determine if dependency has been changed.
*/
protected function generateDependencyData()
diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php
index e565cad..cc1a871 100644
--- a/framework/caching/FileCache.php
+++ b/framework/caching/FileCache.php
@@ -7,7 +7,7 @@
namespace yii\caching;
-use yii\base\InvalidConfigException;
+use Yii;
/**
* FileCache implements a cache component using files.
@@ -51,7 +51,7 @@ class FileCache extends Cache
public function init()
{
parent::init();
- $this->cachePath = \Yii::getAlias($this->cachePath);
+ $this->cachePath = Yii::getAlias($this->cachePath);
if (!is_dir($this->cachePath)) {
mkdir($this->cachePath, 0777, true);
}
diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php
index df07b8e..20aff21 100644
--- a/framework/caching/MemCache.php
+++ b/framework/caching/MemCache.php
@@ -106,7 +106,7 @@ class MemCache extends Cache
/**
* Returns the underlying memcache (or memcached) object.
* @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()
{
diff --git a/framework/caching/ZendDataCache.php b/framework/caching/ZendDataCache.php
index 669733d..5b41a8d 100644
--- a/framework/caching/ZendDataCache.php
+++ b/framework/caching/ZendDataCache.php
@@ -10,7 +10,7 @@ namespace yii\caching;
/**
* 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.
*
* See [[Cache]] for common cache operations that ZendDataCache supports.
diff --git a/framework/console/Application.php b/framework/console/Application.php
index 574495b..2f28cac 100644
--- a/framework/console/Application.php
+++ b/framework/console/Application.php
@@ -129,6 +129,7 @@ class Application extends \yii\base\Application
'migrate' => 'yii\console\controllers\MigrateController',
'app' => 'yii\console\controllers\AppController',
'cache' => 'yii\console\controllers\CacheController',
+ 'asset' => 'yii\console\controllers\AssetController',
);
}
diff --git a/framework/console/Controller.php b/framework/console/Controller.php
index 9924822..c7c5642 100644
--- a/framework/console/Controller.php
+++ b/framework/console/Controller.php
@@ -24,7 +24,6 @@ use yii\base\InvalidRouteException;
* ~~~
*
* @author Qiang Xue
- *
* @since 2.0
*/
class Controller extends \yii\base\Controller
diff --git a/framework/console/controllers/AppController.php b/framework/console/controllers/AppController.php
index 93ef5f5..a47acfe 100644
--- a/framework/console/controllers/AppController.php
+++ b/framework/console/controllers/AppController.php
@@ -86,7 +86,7 @@ class AppController extends Controller
$sourceDir = $this->getSourceDir();
$config = $this->getConfig();
- $list = FileHelper::buildFileList($sourceDir, $path);
+ $list = $this->buildFileList($sourceDir, $path);
if(is_array($config)) {
foreach($config as $file => $settings) {
@@ -96,7 +96,7 @@ class AppController extends Controller
}
}
- FileHelper::copyFiles($list);
+ $this->copyFiles($list);
if(is_array($config)) {
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 $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)
{
@@ -204,4 +204,121 @@ class AppController extends Controller
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:
+ *
+ *
source: required, the full path of the file/directory to be copied from
+ *
target: required, the full path of the file/directory to be copied to
+ *
callback: optional, the callback to be invoked when copying a file. The callback function
+ * should be declared as follows:
+ *
+ * function foo($source,$params)
+ *
+ * where $source parameter is the source file path, and the content returned
+ * by the function will be saved into the target file.
+ *
params: optional, the parameters to be passed to the callback
+ *
+ * @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;
+ }
}
\ No newline at end of file
diff --git a/framework/console/controllers/AssetController.php b/framework/console/controllers/AssetController.php
new file mode 100644
index 0000000..71a2cae
--- /dev/null
+++ b/framework/console/controllers/AssetController.php
@@ -0,0 +1,353 @@
+
+ * @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, <<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 = << 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);
+ }
+}
\ No newline at end of file
diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php
index ea7e3d5..74c354b 100644
--- a/framework/console/controllers/HelpController.php
+++ b/framework/console/controllers/HelpController.php
@@ -9,9 +9,9 @@ namespace yii\console\controllers;
use Yii;
use yii\base\Application;
-use yii\console\Exception;
use yii\base\InlineAction;
use yii\console\Controller;
+use yii\console\Exception;
use yii\console\Request;
use yii\helpers\StringHelper;
@@ -128,7 +128,7 @@ class HelpController extends Controller
$files = scandir($module->getControllerPath());
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));
}
}
diff --git a/framework/console/webapp/default/index.php b/framework/console/webapp/default/index.php
index 461b364..b84e257 100644
--- a/framework/console/webapp/default/index.php
+++ b/framework/console/webapp/default/index.php
@@ -1,10 +1,10 @@
run();
\ No newline at end of file
diff --git a/framework/console/webapp/default/protected/config/main.php b/framework/console/webapp/default/protected/config/main.php
index 1e3f981..795811e 100644
--- a/framework/console/webapp/default/protected/config/main.php
+++ b/framework/console/webapp/default/protected/config/main.php
@@ -1,5 +1,6 @@
'webapp',
'name' => 'My Web Application',
'components' => array(
@@ -12,5 +13,8 @@ return array(
'password' => '',
),
*/
+ 'cache' => array(
+ 'class' => 'yii\caching\DummyCache',
+ ),
),
);
\ No newline at end of file
diff --git a/framework/console/webapp/default/protected/views/layouts/main.php b/framework/console/webapp/default/protected/views/layouts/main.php
index 197b4a2..5c883e6 100644
--- a/framework/console/webapp/default/protected/views/layouts/main.php
+++ b/framework/console/webapp/default/protected/views/layouts/main.php
@@ -1,11 +1,12 @@
+
-
+
- context->pageTitle?>
+ title)?>
-
context->pageTitle?>
+
title)?>
diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php
index d8f2f65..45c53fb 100644
--- a/framework/db/ActiveRecord.php
+++ b/framework/db/ActiveRecord.php
@@ -191,15 +191,12 @@ class ActiveRecord extends Model
*/
public static function updateAllCounters($counters, $condition = '', $params = array())
{
- $db = static::getDb();
$n = 0;
foreach ($counters as $name => $value) {
- $quotedName = $db->quoteColumnName($name);
- $counters[$name] = new Expression("$quotedName+:bp{$n}");
- $params[":bp{$n}"] = $value;
+ $counters[$name] = new Expression("[[$name]]+:bp{$n}", array(":bp{$n}" => $value));
$n++;
}
- $command = $db->createCommand();
+ $command = static::getDb()->createCommand();
$command->update(static::tableName(), $counters, $condition, $params);
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.
* This method is overridden so that attributes and related objects can be accessed like properties.
* @param string $name property name
@@ -530,8 +555,8 @@ class ActiveRecord extends Model
*/
public function isAttributeChanged($name)
{
- if (isset($this->_attribute[$name], $this->_oldAttributes[$name])) {
- return $this->_attribute[$name] !== $this->_oldAttributes[$name];
+ if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
+ return $this->_attributes[$name] !== $this->_oldAttributes[$name];
} else {
return isset($this->_attributes[$name]) || isset($this->_oldAttributes);
}
@@ -590,7 +615,11 @@ class ActiveRecord extends Model
*/
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();
* ~~~
*
+ * 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.
* 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,
* 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)
{
@@ -706,15 +750,31 @@ class ActiveRecord extends Model
if ($this->beforeSave(false)) {
$values = $this->getDirtyAttributes($attributes);
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
// 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) {
$this->_oldAttributes[$name] = $this->_attributes[$name];
}
+
$this->afterSave(false);
+ return $rows;
+ } else {
+ return 0;
}
- return true;
} else {
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]]
* 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()
{
if ($this->beforeDelete()) {
// 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
- $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->afterDelete();
- return true;
+ return $rows;
} else {
return false;
}
diff --git a/framework/db/Command.php b/framework/db/Command.php
index ecd3674..dc6c972 100644
--- a/framework/db/Command.php
+++ b/framework/db/Command.php
@@ -84,39 +84,51 @@ class Command extends \yii\base\Component
/**
* 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.
* @return Command this command instance
*/
public function setSql($sql)
{
if ($sql !== $this->_sql) {
- if ($this->db->enableAutoQuoting && $sql != '') {
- $sql = $this->expandSql($sql);
- }
$this->cancel();
- $this->_sql = $sql;
+ $this->_sql = $this->db->quoteSql($sql);
$this->_params = array();
}
return $this;
}
/**
- * Expands a SQL statement by quoting table and column names and replacing table prefixes.
- * @param string $sql the SQL to be expanded
- * @return string the expanded SQL
+ * Returns the raw SQL by inserting parameter values into the corresponding placeholders in [[sql]].
+ * Note that the return value of this method should mainly be used for logging purpose.
+ * 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;
- return preg_replace_callback('/(\\{\\{(.*?)\\}\\}|\\[\\[(.*?)\\]\\])/', function($matches) use($db) {
- if (isset($matches[3])) {
- return $db->quoteColumnName($matches[3]);
+ if ($this->_params === array()) {
+ return $this->_sql;
+ } else {
+ $params = array();
+ 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 {
- $name = str_replace('%', $db->tablePrefix, $matches[2]);
- return $db->quoteTableName($name);
+ return strtr($this->_sql, $params);
}
- }, $sql);
+ }
}
/**
@@ -134,7 +146,7 @@ class Command extends \yii\base\Component
try {
$this->pdoStatement = $this->db->pdo->prepare($sql);
} 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;
throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode());
}
@@ -243,6 +255,7 @@ class Command extends \yii\base\Component
'boolean' => \PDO::PARAM_BOOL,
'integer' => \PDO::PARAM_INT,
'string' => \PDO::PARAM_STR,
+ 'resource' => \PDO::PARAM_LOB,
'NULL' => \PDO::PARAM_NULL,
);
$type = gettype($data);
@@ -260,21 +273,18 @@ class Command extends \yii\base\Component
{
$sql = $this->getSql();
- if ($this->_params === array()) {
- $paramLog = '';
- } else {
- $paramLog = "\nParameters: " . var_export($this->_params, true);
- }
+ $rawSql = $this->getRawSql();
- Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__);
+ Yii::trace("Executing SQL: $rawSql", __METHOD__);
if ($sql == '') {
return 0;
}
try {
+ $token = "SQL: $sql";
if ($this->db->enableProfiling) {
- Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__);
+ Yii::beginProfile($token, __METHOD__);
}
$this->prepare();
@@ -282,16 +292,16 @@ class Command extends \yii\base\Component
$n = $this->pdoStatement->rowCount();
if ($this->db->enableProfiling) {
- Yii::endProfile(__METHOD__ . "($sql)", __CLASS__);
+ Yii::endProfile($token, __METHOD__);
}
return $n;
} catch (\Exception $e) {
if ($this->db->enableProfiling) {
- Yii::endProfile(__METHOD__ . "($sql)", __CLASS__);
+ Yii::endProfile($token, __METHOD__);
}
$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;
throw new Exception($message, $errorInfo, (int)$e->getCode());
@@ -377,13 +387,9 @@ class Command extends \yii\base\Component
{
$db = $this->db;
$sql = $this->getSql();
- if ($this->_params === array()) {
- $paramLog = '';
- } else {
- $paramLog = "\nParameters: " . var_export($this->_params, true);
- }
+ $rawSql = $this->getRawSql();
- Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__);
+ Yii::trace("Querying SQL: $rawSql", __METHOD__);
/** @var $cache \yii\caching\Cache */
if ($db->enableQueryCache && $method !== '') {
@@ -395,18 +401,18 @@ class Command extends \yii\base\Component
__CLASS__,
$db->dsn,
$db->username,
- $sql,
- $paramLog,
+ $rawSql,
));
if (($result = $cache->get($cacheKey)) !== false) {
- Yii::trace('Query result served from cache', __CLASS__);
+ Yii::trace('Query result served from cache', __METHOD__);
return $result;
}
}
try {
+ $token = "SQL: $sql";
if ($db->enableProfiling) {
- Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__);
+ Yii::beginProfile($token, __METHOD__);
}
$this->prepare();
@@ -423,21 +429,21 @@ class Command extends \yii\base\Component
}
if ($db->enableProfiling) {
- Yii::endProfile(__METHOD__ . "($sql)", __CLASS__);
+ Yii::endProfile($token, __METHOD__);
}
if (isset($cache, $cacheKey) && $cache instanceof Cache) {
$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;
} catch (\Exception $e) {
if ($db->enableProfiling) {
- Yii::endProfile(__METHOD__ . "($sql)", __CLASS__);
+ Yii::endProfile($token, __METHOD__);
}
$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;
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())
{
- $sql = $this->db->getQueryBuilder()->delete($table, $condition);
+ $sql = $this->db->getQueryBuilder()->delete($table, $condition, $params);
return $this->setSql($sql)->bindValues($params);
}
diff --git a/framework/db/Connection.php b/framework/db/Connection.php
index 59e8422..797508a 100644
--- a/framework/db/Connection.php
+++ b/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
* as `{{%TableName}}`, then the percentage character `%` will be replaced with this
* property value. For example, `{{%post}}` becomes `{{tbl_post}}` if this property is
- * set as `"tbl_"`. Note that this property is only effective when [[enableAutoQuoting]]
- * is true.
- * @see enableAutoQuoting
+ * set as `"tbl_"`.
*/
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.
* 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
@@ -248,15 +237,15 @@ class Connection extends Component
* [[Schema]] class to support DBMS that is not supported by Yii.
*/
public $schemaMap = array(
- 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL
- 'mysqli' => 'yii\db\mysql\Schema', // MySQL
- 'mysql' => 'yii\db\mysql\Schema', // MySQL
- 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3
+ 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL
+ 'mysqli' => 'yii\db\mysql\Schema', // MySQL
+ 'mysql' => 'yii\db\mysql\Schema', // MySQL
+ 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3
'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2
'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
- 'oci' => 'yii\db\oci\Schema', // Oracle driver
+ 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql
+ '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
@@ -324,12 +313,12 @@ class Connection extends Component
throw new InvalidConfigException('Connection::dsn cannot be empty.');
}
try {
- \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__);
+ \Yii::trace('Opening DB connection: ' . $this->dsn, __METHOD__);
$this->pdo = $this->createPdoInstance();
$this->initConnection();
}
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.';
throw new Exception($message, $e->errorInfo, (int)$e->getCode());
}
@@ -343,7 +332,7 @@ class Connection extends Component
public function close()
{
if ($this->pdo !== null) {
- \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__);
+ \Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__);
$this->pdo = null;
$this->_schema = 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]].
* @return string name of the DB driver
*/
diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php
index 75375cc..441d287 100644
--- a/framework/db/QueryBuilder.php
+++ b/framework/db/QueryBuilder.php
@@ -22,6 +22,11 @@ use yii\base\NotSupportedException;
class QueryBuilder extends \yii\base\Object
{
/**
+ * The prefix for automatically generated query binding parameters.
+ */
+ const PARAM_PREFIX = ':qp';
+
+ /**
* @var Connection the database connection.
*/
public $db;
@@ -58,11 +63,11 @@ class QueryBuilder extends \yii\base\Object
$clauses = array(
$this->buildSelect($query->select, $query->distinct, $query->selectOption),
$this->buildFrom($query->from),
- $this->buildJoin($query->join),
- $this->buildWhere($query->where),
+ $this->buildJoin($query->join, $query->params),
+ $this->buildWhere($query->where, $query->params),
$this->buildGroupBy($query->groupBy),
- $this->buildHaving($query->having),
- $this->buildUnion($query->union),
+ $this->buildHaving($query->having, $query->params),
+ $this->buildUnion($query->union, $query->params),
$this->buildOrderBy($query->orderBy),
$this->buildLimit($query->limit, $query->offset),
);
@@ -92,7 +97,6 @@ class QueryBuilder extends \yii\base\Object
{
$names = array();
$placeholders = array();
- $count = 0;
foreach ($columns as $name => $value) {
$names[] = $this->db->quoteColumnName($name);
if ($value instanceof Expression) {
@@ -101,9 +105,9 @@ class QueryBuilder extends \yii\base\Object
$params[$n] = $v;
}
} else {
- $placeholders[] = ':p' . $count;
- $params[':p' . $count] = $value;
- $count++;
+ $phName = self::PARAM_PREFIX . count($params);
+ $placeholders[] = $phName;
+ $params[$phName] = $value;
}
}
@@ -159,10 +163,9 @@ class QueryBuilder extends \yii\base\Object
* so that they can be bound to the DB command later.
* @return string the UPDATE SQL
*/
- public function update($table, $columns, $condition = '', &$params)
+ public function update($table, $columns, $condition, &$params)
{
$lines = array();
- $count = 0;
foreach ($columns as $name => $value) {
if ($value instanceof Expression) {
$lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression;
@@ -170,17 +173,15 @@ class QueryBuilder extends \yii\base\Object
$params[$n] = $v;
}
} else {
- $lines[] = $this->db->quoteColumnName($name) . '=:p' . $count;
- $params[':p' . $count] = $value;
- $count++;
+ $phName = self::PARAM_PREFIX . count($params);
+ $lines[] = $this->db->quoteColumnName($name) . '=' . $phName;
+ $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 mixed $condition the condition that will be put in the WHERE part. Please
* 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
*/
- public function delete($table, $condition = '')
+ public function delete($table, $condition, &$params)
{
$sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
- if (($where = $this->buildCondition($condition)) !== '') {
- $sql .= ' WHERE ' . $where;
- }
- return $sql;
+ $where = $this->buildWhere($condition, $params);
+ return $where === '' ? $sql : $sql . ' ' . $where;
}
/**
@@ -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 boolean $distinct
* @param string $selectOption
@@ -737,10 +544,11 @@ class QueryBuilder extends \yii\base\Object
/**
* @param string|array $joins
+ * @param array $params the binding parameters to be populated
* @return string the JOIN clause built from [[query]].
* @throws Exception if the $joins parameter is not in proper format
*/
- public function buildJoin($joins)
+ public function buildJoin($joins, &$params)
{
if (empty($joins)) {
return '';
@@ -761,9 +569,9 @@ class QueryBuilder extends \yii\base\Object
}
$joins[$i] = $join[0] . ' ' . $table;
if (isset($join[2])) {
- $condition = $this->buildCondition($join[2]);
+ $condition = $this->buildCondition($join[2], $params);
if ($condition !== '') {
- $joins[$i] .= ' ON ' . $this->buildCondition($join[2]);
+ $joins[$i] .= ' ON ' . $condition;
}
}
} else {
@@ -776,11 +584,12 @@ class QueryBuilder extends \yii\base\Object
/**
* @param string|array $condition
+ * @param array $params the binding parameters to be populated
* @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;
}
@@ -795,11 +604,12 @@ class QueryBuilder extends \yii\base\Object
/**
* @param string|array $condition
+ * @param array $params the binding parameters to be populated
* @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;
}
@@ -843,16 +653,19 @@ class QueryBuilder extends \yii\base\Object
/**
* @param array $unions
+ * @param array $params the binding parameters to be populated
* @return string the UNION clause built from [[query]].
*/
- public function buildUnion($unions)
+ public function buildUnion($unions, &$params)
{
if (empty($unions)) {
return '';
}
foreach ($unions as $i => $union) {
if ($union instanceof Query) {
+ $union->addParams($params);
$unions[$i] = $this->build($union);
+ $params = $union->params;
}
}
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
* @return string the processing result
*/
- protected function buildColumns($columns)
+ public function buildColumns($columns)
{
if (!is_array($columns)) {
if (strpos($columns, '(') !== false) {
@@ -882,4 +695,218 @@ class QueryBuilder extends \yii\base\Object
}
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);
+ }
}
diff --git a/framework/db/Schema.php b/framework/db/Schema.php
index 71bc9a2..9538e4c 100644
--- a/framework/db/Schema.php
+++ b/framework/db/Schema.php
@@ -83,7 +83,7 @@ abstract class Schema extends \yii\base\Object
}
$db = $this->db;
- $realName = $this->getRealTableName($name);
+ $realName = $this->getRawTableName($name);
if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) {
/** @var $cache Cache */
@@ -248,7 +248,7 @@ abstract class Schema extends \yii\base\Object
/**
* 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 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.
* @param string $name table name
* @return string the properly quoted table name
@@ -256,7 +256,7 @@ abstract class Schema extends \yii\base\Object
*/
public function quoteTableName($name)
{
- if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) {
+ if (strpos($name, '(') !== false || strpos($name, '{{') !== false) {
return $name;
}
if (strpos($name, '.') === false) {
@@ -273,7 +273,7 @@ abstract class Schema extends \yii\base\Object
/**
* 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 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.
* @param string $name 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
- * 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
* @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);
return str_replace('%', $this->db->tablePrefix, $name);
} else {
diff --git a/framework/db/StaleObjectException.php b/framework/db/StaleObjectException.php
new file mode 100644
index 0000000..860c9fc
--- /dev/null
+++ b/framework/db/StaleObjectException.php
@@ -0,0 +1,23 @@
+
+ * @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');
+ }
+}
\ No newline at end of file
diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php
index 177d2cb..d66c38e 100644
--- a/framework/db/Transaction.php
+++ b/framework/db/Transaction.php
@@ -66,7 +66,7 @@ class Transaction extends \yii\base\Object
if ($this->db === null) {
throw new InvalidConfigException('Transaction::db must be set.');
}
- \Yii::trace('Starting transaction', __CLASS__);
+ \Yii::trace('Starting transaction', __METHOD__);
$this->db->open();
$this->db->pdo->beginTransaction();
$this->_active = true;
@@ -80,7 +80,7 @@ class Transaction extends \yii\base\Object
public function commit()
{
if ($this->_active && $this->db && $this->db->isActive) {
- \Yii::trace('Committing transaction', __CLASS__);
+ \Yii::trace('Committing transaction', __METHOD__);
$this->db->pdo->commit();
$this->_active = false;
} else {
@@ -95,7 +95,7 @@ class Transaction extends \yii\base\Object
public function rollback()
{
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->_active = false;
} else {
diff --git a/framework/helpers/ArrayHelper.php b/framework/helpers/ArrayHelper.php
index 65fa962..3061717 100644
--- a/framework/helpers/ArrayHelper.php
+++ b/framework/helpers/ArrayHelper.php
@@ -7,9 +7,6 @@
namespace yii\helpers;
-use Yii;
-use yii\base\InvalidParamException;
-
/**
* ArrayHelper provides additional array functionality you can use in your
* application.
@@ -17,324 +14,6 @@ use yii\base\InvalidParamException;
* @author Qiang Xue
* @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;
- }
}
\ No newline at end of file
diff --git a/framework/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php
index 429aeb1..794b9c8 100644
--- a/framework/helpers/ConsoleColor.php
+++ b/framework/helpers/ConsoleColor.php
@@ -18,453 +18,6 @@ namespace yii\helpers;
* @author Carsten Brandt
* @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 .= '';
- }
- 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 '';
- }, $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.
- *
- * 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 %
- *
- * 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);
- }
}
diff --git a/framework/helpers/FileHelper.php b/framework/helpers/FileHelper.php
index f850b98..3fb24e1 100644
--- a/framework/helpers/FileHelper.php
+++ b/framework/helpers/FileHelper.php
@@ -9,9 +9,6 @@
namespace yii\helpers;
-use yii\base\Exception;
-use yii\base\InvalidConfigException;
-
/**
* Filesystem helper
*
@@ -19,256 +16,6 @@ use yii\base\InvalidConfigException;
* @author Alex Makarov
* @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:
- *
- *
source: required, the full path of the file/directory to be copied from
- *
target: required, the full path of the file/directory to be copied to
- *
callback: optional, the callback to be invoked when copying a file. The callback function
- * should be declared as follows:
- *
- * function foo($source,$params)
- *
- * where $source parameter is the source file path, and the content returned
- * by the function will be saved into the target file.
- *
params: optional, the parameters to be passed to the callback
- *
- * @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;
- }
}
\ No newline at end of file
diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php
index b2ca576..b3a0743 100644
--- a/framework/helpers/Html.php
+++ b/framework/helpers/Html.php
@@ -7,975 +7,12 @@
namespace yii\helpers;
-use Yii;
-use yii\base\InvalidParamException;
-
/**
* Html provides a set of static methods for generating commonly used HTML tags.
*
* @author Qiang Xue
* @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 '';
- }
-
- /**
- * 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', "/**/", $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', "/**/", $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 '';
- }
-
- /**
- * 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(' ', ' ', 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(' ', ' ', 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);
- }
- }
}
diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php
index 5029dd6..d3cb2ad 100644
--- a/framework/helpers/SecurityHelper.php
+++ b/framework/helpers/SecurityHelper.php
@@ -7,11 +7,6 @@
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.
*
@@ -29,244 +24,6 @@ use yii\base\InvalidParamException;
* @author Tom Worster
* @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, " 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;
- }
}
\ No newline at end of file
diff --git a/framework/helpers/StringHelper.php b/framework/helpers/StringHelper.php
index ace34db..22b881a 100644
--- a/framework/helpers/StringHelper.php
+++ b/framework/helpers/StringHelper.php
@@ -14,112 +14,6 @@ namespace yii\helpers;
* @author Alex Makarov
* @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('/(?
* @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("/', '', $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;
- }
- }
}
\ No newline at end of file
diff --git a/framework/helpers/base/ArrayHelper.php b/framework/helpers/base/ArrayHelper.php
new file mode 100644
index 0000000..9870542
--- /dev/null
+++ b/framework/helpers/base/ArrayHelper.php
@@ -0,0 +1,340 @@
+
+ * @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;
+ }
+}
\ No newline at end of file
diff --git a/framework/helpers/base/ConsoleColor.php b/framework/helpers/base/ConsoleColor.php
new file mode 100644
index 0000000..5e7f577
--- /dev/null
+++ b/framework/helpers/base/ConsoleColor.php
@@ -0,0 +1,470 @@
+
+ * @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 .= '';
+ }
+ 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 '';
+ }, $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.
+ *
+ * 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 %
+ *
- * 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:
- *
- *
- * You can use {@link CFileValidator} to validate the file attribute.
+ * FileValidator verifies if an attribute is receiving a valid uploaded file.
*
* @author Qiang Xue
* @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.
* This can be either an array or a string consisting of file extension names
* separated by space or comma (e.g. "gif, jpg").
@@ -66,136 +38,179 @@ class CFileValidator extends Validator
* Defaults to null, meaning no limit.
* Note, the size limit is also affected by 'upload_max_filesize' INI setting
* and the 'MAX_FILE_SIZE' hidden field value.
- * @see tooLarge
+ * @see tooBig
*/
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.
- * @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.
- * @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;
/**
* @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;
/**
- * @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 if the count of multiple uploads exceeds
- * limit.
+ * @var string the error message used if the count of multiple uploads exceeds limit.
+ * You may use the following tokens in the message:
+ *
+ * - {attribute}: the attribute name
+ * - {file}: the uploaded file name
+ * - {limit}: the value of [[maxFiles]]
*/
public $tooMany;
/**
- * Set the attribute and then validates using {@link validateFile}.
- * If there is any error, the error message is added to the object.
- * @param \yii\base\Model $object the object being validated
- * @param string $attribute the attribute being validated
+ * Initializes the validator.
*/
- public function validateAttribute($object, $attribute)
+ public function init()
{
- if ($this->maxFiles > 1)
- {
- $files = $object->$attribute;
- if (!is_array($files) || !isset($files[0]) || !$files[0] instanceof CUploadedFile)
- $files = CUploadedFile::getInstances($object, $attribute);
- if (array() === $files)
- return $this->emptyAttribute($object, $attribute);
- if (count($files) > $this->maxFiles)
- {
- $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii|{attribute} cannot accept more than {limit} files.');
- $this->addError($object, $attribute, $message, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles));
- } else
- foreach ($files as $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);
- }
- $this->validateFile($object, $attribute, $file);
+ 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);
+ }
}
/**
- * Internally validates a file object.
+ * Validates the attribute.
* @param \yii\base\Model $object the object being validated
* @param string $attribute the attribute being validated
- * @param CUploadedFile $file uploaded file passed to check against a set of rules
*/
- public function validateFile($object, $attribute, $file)
+ public function validateAttribute($object, $attribute)
{
- if (null === $file || ($error = $file->getError()) == UPLOAD_ERR_NO_FILE)
- return $this->emptyAttribute($object, $attribute);
- elseif ($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE || $this->maxSize !== null && $file->getSize() > $this->maxSize)
- {
- $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->maxFiles > 1) {
+ $files = $object->$attribute;
+ if (!is_array($files)) {
+ $this->addError($object, $attribute, $this->uploadRequired);
+ return;
+ }
+ foreach ($files as $i => $file) {
+ if (!$file instanceof UploadedFile || $file->getError() == UPLOAD_ERR_NO_FILE) {
+ unset($files[$i]);
+ }
+ }
+ $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);
+ }
+ }
+ } else {
+ $file = $object->$attribute;
+ if ($file instanceof UploadedFile && $file->getError() != UPLOAD_ERR_NO_FILE) {
+ $this->validateFile($object, $attribute, $file);
+ } else {
+ $this->addError($object, $attribute, $this->uploadRequired);
}
}
}
/**
- * Raises an error to inform end user about blank attribute.
+ * Internally validates a file object.
* @param \yii\base\Model $object the object being validated
* @param string $attribute the attribute being validated
+ * @param UploadedFile $file uploaded file passed to check against a set of rules
*/
- public function emptyAttribute($object, $attribute)
+ protected function validateFile($object, $attribute, $file)
{
- if (!$this->allowEmpty)
- {
- $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} cannot be blank.');
- $this->addError($object, $attribute, $message);
+ switch ($file->getError()) {
+ case UPLOAD_ERR_OK:
+ if ($this->maxSize !== null && $file->getSize() > $this->maxSize) {
+ $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit()));
+ }
+ 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:
+ case UPLOAD_ERR_FORM_SIZE:
+ $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit()));
+ break;
+ case UPLOAD_ERR_PARTIAL:
+ $this->addError($object, $attribute, $this->message);
+ Yii::warning('File was only partially uploaded: ' . $file->getName(), __METHOD__);
+ break;
+ case UPLOAD_ERR_NO_TMP_DIR:
+ $this->addError($object, $attribute, $this->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.
* This is determined based on three factors:
- *
*
- * @since 1.1.5
+ * ~~~
+ * Options FollowSymLinks
+ * ~~~
*/
- public $linkAssets=false;
+ public $linkAssets = false;
/**
- * @var array list of directories and files which should be excluded from the publishing process.
- * Defaults to exclude '.svn' and '.gitignore' files only. This option has no effect if {@link linkAssets} is enabled.
- * @since 1.1.6
- **/
- public $excludeFiles=array('.svn','.gitignore');
- /**
- * @var integer the permission to be set for newly generated asset files.
- * This value will be used by PHP chmod function.
- * Defaults to 0666, meaning the file is read-writable by all users.
- * @since 1.1.8
+ * @var integer the permission to be set for newly published asset files.
+ * This value will be used by PHP chmod() function.
+ * If not set, the permission will be determined by the current environment.
*/
- public $newFileMode=0666;
+ public $fileMode;
/**
* @var integer the permission to be set for newly generated asset directories.
- * This value will be used by PHP chmod function.
+ * This value will be used by PHP chmod() function.
* Defaults to 0777, meaning the directory can be read, written and executed by all users.
- * @since 1.1.8
- */
- public $newDirMode=0777;
- /**
- * @var string base web accessible path for storing private files
*/
- private $_basePath;
- /**
- * @var string base URL for accessing the publishing directory.
- */
- private $_baseUrl;
- /**
- * @var array published assets
- */
- private $_published=array();
+ public $dirMode = 0777;
/**
- * @return string the root directory storing the published asset files. Defaults to 'WebRoot/assets'.
+ * Initializes the component.
+ * @throws InvalidConfigException if [[basePath]] is invalid
*/
- public function getBasePath()
+ public function init()
{
- if($this->_basePath===null)
- {
- $request=\Yii::$app->getRequest();
- $this->setBasePath(dirname($request->getScriptFile()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH);
+ parent::init();
+ $this->basePath = Yii::getAlias($this->basePath);
+ if (!is_dir($this->basePath)) {
+ throw new InvalidConfigException("The directory does not exist: {$this->basePath}");
+ } elseif (!is_writable($this->basePath)) {
+ throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}");
+ } else {
+ $this->basePath = realpath($this->basePath);
+ }
+ $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/');
+
+ foreach (require(YII_PATH . '/assets.php') as $name => $bundle) {
+ if (!isset($this->bundles[$name])) {
+ $this->bundles[$name] = $bundle;
+ }
}
- return $this->_basePath;
}
/**
- * Sets the root directory storing published asset files.
- * @param string $value the root directory storing published asset files
- * @throws CException if the base path is invalid
+ * Returns the named bundle.
+ * This method will first look for the bundle in [[bundles]]. If not found,
+ * it will attempt to find the bundle from an installed extension using the following procedure:
+ *
+ * 1. Convert the bundle into a path alias;
+ * 2. Determine the root alias and use it to locate the bundle manifest file "assets.php";
+ * 3. Look for the bundle in the manifest file.
+ *
+ * For example, given the bundle name "foo/button", the method will first convert it
+ * into the path alias "@foo/button"; since "@foo" is the root alias, it will look
+ * for the bundle manifest file "@foo/assets.php". The manifest file should return an array
+ * that lists the bundles used by the "foo/button" extension. The array format is the same as [[bundles]].
+ *
+ * @param string $name the bundle name
+ * @return AssetBundle the loaded bundle object. Null is returned if the bundle does not exist.
*/
- public function setBasePath($value)
+ public function getBundle($name)
{
- if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath))
- $this->_basePath=$basePath;
- else
- throw new CException(Yii::t('yii|CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.',
- array('{path}'=>$value)));
+ if (!isset($this->bundles[$name])) {
+ $rootAlias = Yii::getRootAlias("@$name");
+ if ($rootAlias !== false) {
+ $manifest = Yii::getAlias("$rootAlias/assets.php", false);
+ if ($manifest !== false && is_file($manifest)) {
+ foreach (require($manifest) as $bn => $config) {
+ $this->bundles[$bn] = $config;
+ }
+ }
+ }
+ if (!isset($this->bundles[$name])) {
+ return null;
+ }
+ }
+ if (is_array($this->bundles[$name])) {
+ $config = $this->bundles[$name];
+ if (!isset($config['class'])) {
+ $config['class'] = 'yii\\web\\AssetBundle';
+ $this->bundles[$name] = Yii::createObject($config);
+ }
+ }
+
+ return $this->bundles[$name];
}
+ private $_converter;
+
/**
- * @return string the base url that the published asset files can be accessed.
- * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'.
+ * Returns the asset converter.
+ * @return IAssetConverter the asset converter.
*/
- public function getBaseUrl()
+ public function getConverter()
{
- if($this->_baseUrl===null)
- {
- $request=\Yii::$app->getRequest();
- $this->setBaseUrl($request->getBaseUrl().'/'.self::DEFAULT_BASEPATH);
+ if ($this->_converter === null) {
+ $this->_converter = Yii::createObject(array(
+ 'class' => 'yii\\web\\AssetConverter',
+ ));
+ } elseif (is_array($this->_converter) || is_string($this->_converter)) {
+ $this->_converter = Yii::createObject($this->_converter);
}
- return $this->_baseUrl;
+ return $this->_converter;
}
/**
- * @param string $value the base url that the published asset files can be accessed
+ * Sets the asset converter.
+ * @param array|IAssetConverter $value the asset converter. This can be either
+ * an object implementing the [[IAssetConverter]] interface, or a configuration
+ * array that can be used to create the asset converter object.
*/
- public function setBaseUrl($value)
+ public function setConverter($value)
{
- $this->_baseUrl=rtrim($value,'/');
+ $this->_converter = $value;
}
/**
+ * @var array published assets
+ */
+ private $_published = array();
+
+ /**
* Publishes a file or a directory.
- * This method will copy the specified asset to a web accessible directory
- * and return the URL for accessing the published asset.
- *
- *
If the asset is a file, its file modification time will be checked
- * to avoid unnecessary file copying;
- *
If the asset is a directory, all files and subdirectories under it will
- * be published recursively. Note, in case $forceCopy is false the method only checks the
- * existence of the target directory to avoid repetitive copying.
- *
+ *
+ * This method will copy the specified file or directory to [[basePath]] so that
+ * it can be accessed via the Web server.
+ *
+ * If the asset is a file, its file modification time will be checked to avoid
+ * unnecessary file copying.
+ *
+ * If the asset is a directory, all files and subdirectories under it will be published recursively.
+ * Note, in case $forceCopy is false the method only checks the existence of the target
+ * directory to avoid repetitive copying (which is very expensive).
+ *
+ * By default, when publishing a directory, subdirectories and files whose name starts with a dot "."
+ * will NOT be published. If you want to change this behavior, you may specify the "beforeCopy" option
+ * as explained in the `$options` parameter.
*
* Note: On rare scenario, a race condition can develop that will lead to a
* one-time-manifestation of a non-critical problem in the creation of the directory
@@ -157,85 +192,85 @@ class CAssetManager extends CApplicationComponent
* discussion: http://code.google.com/p/yii/issues/detail?id=2579
*
* @param string $path the asset (file or directory) to be published
- * @param boolean $hashByName whether the published directory should be named as the hashed basename.
- * If false, the name will be the hash taken from dirname of the path being published and path mtime.
- * Defaults to false. Set true if the path being published is shared among
- * different extensions.
- * @param integer $level level of recursive copying when the asset is a directory.
- * Level -1 means publishing all subdirectories and files;
- * Level 0 means publishing only the files DIRECTLY under the directory;
- * level N means copying those directories that are within N levels.
- * @param boolean $forceCopy whether we should copy the asset file or directory even if it is already published before.
- * This parameter is set true mainly during development stage when the original
- * assets are being constantly changed. The consequence is that the performance
- * is degraded, which is not a concern during development, however.
- * This parameter has been available since version 1.1.2.
- * @return string an absolute URL to the published asset
- * @throws CException if the asset to be published does not exist.
+ * @param array $options the options to be applied when publishing a directory.
+ * The following options are supported:
+ *
+ * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
+ * This option is used only when publishing a directory. 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.
+ * This option is used only when publishing a directory. The signature of the callback is similar to that
+ * of `beforeCopy`.
+ * - forceCopy: boolean, whether the directory being published should be copied even if
+ * it is found in the target directory. This option is used only when publishing a directory.
+ * You may want to set this to be true during the development stage to make sure the published
+ * directory is always up-to-date. Do not set this to true on production servers as it will
+ * significantly degrade the performance.
+ * @return array the path (directory or file path) and the URL that the asset is published as.
+ * @throws InvalidParamException if the asset to be published does not exist.
*/
- public function publish($path,$hashByName=false,$level=-1,$forceCopy=false)
+ public function publish($path, $options = array())
{
- if(isset($this->_published[$path]))
+ if (isset($this->_published[$path])) {
return $this->_published[$path];
- else if(($src=realpath($path))!==false)
- {
- if(is_file($src))
- {
- $dir=$this->hash($hashByName ? basename($src) : dirname($src).filemtime($src));
- $fileName=basename($src);
- $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir;
- $dstFile=$dstDir.DIRECTORY_SEPARATOR.$fileName;
+ }
- if($this->linkAssets)
- {
- if(!is_file($dstFile))
- {
- if(!is_dir($dstDir))
- {
- mkdir($dstDir);
- @chmod($dstDir, $this->newDirMode);
- }
- symlink($src,$dstFile);
- }
- }
- else if(@filemtime($dstFile)<@filemtime($src))
- {
- if(!is_dir($dstDir))
- {
- mkdir($dstDir);
- @chmod($dstDir, $this->newDirMode);
- }
- copy($src,$dstFile);
- @chmod($dstFile, $this->newFileMode);
- }
+ $src = realpath($path);
+ if ($src === false) {
+ throw new InvalidParamException("The file or directory to be published does not exist: $path");
+ }
- return $this->_published[$path]=$this->getBaseUrl()."/$dir/$fileName";
+ if (is_file($src)) {
+ $dir = $this->hash(dirname($src) . filemtime($src));
+ $fileName = basename($src);
+ $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir;
+ $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName;
+
+ if (!is_dir($dstDir)) {
+ mkdir($dstDir, $this->dirMode, true);
}
- else if(is_dir($src))
- {
- $dir=$this->hash($hashByName ? basename($src) : $src.filemtime($src));
- $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir;
- if($this->linkAssets)
- {
- if(!is_dir($dstDir))
- symlink($src,$dstDir);
+ if ($this->linkAssets) {
+ if (!is_file($dstFile)) {
+ symlink($src, $dstFile);
}
- else if(!is_dir($dstDir) || $forceCopy)
- {
- CFileHelper::copyDirectory($src,$dstDir,array(
- 'exclude'=>$this->excludeFiles,
- 'level'=>$level,
- 'newDirMode'=>$this->newDirMode,
- 'newFileMode'=>$this->newFileMode,
- ));
+ } elseif (@filemtime($dstFile) < @filemtime($src)) {
+ copy($src, $dstFile);
+ if ($this->fileMode !== null) {
+ @chmod($dstFile, $this->fileMode);
}
+ }
- return $this->_published[$path]=$this->getBaseUrl().'/'.$dir;
+ return $this->_published[$path] = array($dstFile, $this->baseUrl . "/$dir/$fileName");
+ } else {
+ $dir = $this->hash($src . filemtime($src));
+ $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir;
+ if ($this->linkAssets) {
+ if (!is_dir($dstDir)) {
+ symlink($src, $dstDir);
+ }
+ } elseif (!is_dir($dstDir) || !empty($options['forceCopy'])) {
+ $opts = array(
+ 'dirMode' => $this->dirMode,
+ 'fileMode' => $this->fileMode,
+ );
+ if (isset($options['beforeCopy'])) {
+ $opts['beforeCopy'] = $options['beforeCopy'];
+ } else {
+ $opts['beforeCopy'] = function ($from, $to) {
+ return strncmp(basename($from), '.', 1) !== 0;
+ };
+ }
+ if (isset($options['afterCopy'])) {
+ $opts['afterCopy'] = $options['afterCopy'];
+ }
+ FileHelper::copyDirectory($src, $dstDir, $opts);
}
+
+ return $this->_published[$path] = array($dstDir, $this->baseUrl . '/' . $dir);
}
- throw new CException(Yii::t('yii|The asset "{asset}" to be published does not exist.',
- array('{asset}'=>$path)));
}
/**
@@ -243,24 +278,20 @@ class CAssetManager extends CApplicationComponent
* This method does not perform any publishing. It merely tells you
* if the file or directory is published, where it will go.
* @param string $path directory or file path being published
- * @param boolean $hashByName whether the published directory should be named as the hashed basename.
- * If false, the name will be the hash taken from dirname of the path being published and path mtime.
- * Defaults to false. Set true if the path being published is shared among
- * different extensions.
* @return string the published file path. False if the file or directory does not exist
*/
- public function getPublishedPath($path,$hashByName=false)
+ public function getPublishedPath($path)
{
- if(($path=realpath($path))!==false)
- {
- $base=$this->getBasePath().DIRECTORY_SEPARATOR;
- if(is_file($path))
- return $base . $this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)) . DIRECTORY_SEPARATOR . basename($path);
- else
- return $base . $this->hash($hashByName ? basename($path) : $path.filemtime($path));
- }
- else
+ if (($path = realpath($path)) !== false) {
+ $base = $this->basePath . DIRECTORY_SEPARATOR;
+ if (is_file($path)) {
+ return $base . $this->hash(dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path);
+ } else {
+ return $base . $this->hash($path . filemtime($path));
+ }
+ } else {
return false;
+ }
}
/**
@@ -268,25 +299,22 @@ class CAssetManager extends CApplicationComponent
* This method does not perform any publishing. It merely tells you
* if the file path is published, what the URL will be to access it.
* @param string $path directory or file path being published
- * @param boolean $hashByName whether the published directory should be named as the hashed basename.
- * If false, the name will be the hash taken from dirname of the path being published and path mtime.
- * Defaults to false. Set true if the path being published is shared among
- * different extensions.
* @return string the published URL for the file or directory. False if the file or directory does not exist.
*/
- public function getPublishedUrl($path,$hashByName=false)
+ public function getPublishedUrl($path)
{
- if(isset($this->_published[$path]))
+ if (isset($this->_published[$path])) {
return $this->_published[$path];
- if(($path=realpath($path))!==false)
- {
- if(is_file($path))
- return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)).'/'.basename($path);
- else
- return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : $path.filemtime($path));
}
- else
+ if (($path = realpath($path)) !== false) {
+ if (is_file($path)) {
+ return $this->baseUrl . '/' . $this->hash(dirname($path) . filemtime($path)) . '/' . basename($path);
+ } else {
+ return $this->baseUrl . '/' . $this->hash($path . filemtime($path));
+ }
+ } else {
return false;
+ }
}
/**
@@ -297,6 +325,6 @@ class CAssetManager extends CApplicationComponent
*/
protected function hash($path)
{
- return sprintf('%x',crc32($path.Yii::getVersion()));
+ return sprintf('%x', crc32($path . Yii::getVersion()));
}
}
diff --git a/framework/web/Controller.php b/framework/web/Controller.php
index 93b74aa..8049299 100644
--- a/framework/web/Controller.php
+++ b/framework/web/Controller.php
@@ -8,6 +8,7 @@
namespace yii\web;
use Yii;
+use yii\helpers\Html;
/**
* Controller is the base class of Web controllers.
@@ -40,5 +41,4 @@ class Controller extends \yii\base\Controller
}
return Yii::$app->getUrlManager()->createUrl($route, $params);
}
-
}
\ No newline at end of file
diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php
index d3afc76..2910b40 100644
--- a/framework/web/DbSession.php
+++ b/framework/web/DbSession.php
@@ -144,7 +144,7 @@ class DbSession extends Session
$query = new Query;
$data = $query->select(array('data'))
->from($this->sessionTable)
- ->where('expire>:expire AND id=:id', array(':expire' => time(), ':id' => $id))
+ ->where('[[expire]]>:expire AND [[id]]=:id', array(':expire' => time(), ':id' => $id))
->createCommand($this->db)
->queryScalar();
return $data === false ? '' : $data;
@@ -214,7 +214,7 @@ class DbSession extends Session
public function gcSession($maxLifetime)
{
$this->db->createCommand()
- ->delete($this->sessionTable, 'expire<:expire', array(':expire' => time()))
+ ->delete($this->sessionTable, '[[expire]]<:expire', array(':expire' => time()))
->execute();
return true;
}
diff --git a/framework/web/IAssetConverter.php b/framework/web/IAssetConverter.php
new file mode 100644
index 0000000..4334d3e
--- /dev/null
+++ b/framework/web/IAssetConverter.php
@@ -0,0 +1,27 @@
+
+ * @since 2.0
+ */
+interface IAssetConverter
+{
+ /**
+ * Converts a given asset file into a CSS or JS file.
+ * @param string $asset the asset file path, relative to $basePath
+ * @param string $basePath the directory the $asset is relative to.
+ * @param string $baseUrl the URL corresponding to $basePath
+ * @return string the URL to the converted asset file. If the given asset does not
+ * need conversion, "$baseUrl/$asset" should be returned.
+ */
+ public function convert($asset, $basePath, $baseUrl);
+}
\ No newline at end of file
diff --git a/framework/web/Identity.php b/framework/web/Identity.php
index 4668337..6d67bc0 100644
--- a/framework/web/Identity.php
+++ b/framework/web/Identity.php
@@ -8,6 +8,35 @@
namespace yii\web;
/**
+ * Identity is the interface that should be implemented by a class providing identity information.
+ *
+ * This interface can typically be implemented by a user model class. For example, the following
+ * code shows how to implement this interface by a User ActiveRecord class:
+ *
+ * ~~~
+ * class User extends ActiveRecord implements Identity
+ * {
+ * public static function findIdentity($id)
+ * {
+ * return static::find($id);
+ * }
+ *
+ * public function getId()
+ * {
+ * return $this->id;
+ * }
+ *
+ * public function getAuthKey()
+ * {
+ * return $this->authKey;
+ * }
+ *
+ * public function validateAuthKey($authKey)
+ * {
+ * return $this->authKey === $authKey;
+ * }
+ * }
+ * ~~~
*
* @author Qiang Xue
* @since 2.0
@@ -15,31 +44,38 @@ namespace yii\web;
interface Identity
{
/**
+ * Finds an identity by the given ID.
+ * @param string|integer $id the ID to be looked for
+ * @return Identity the identity object that matches the given ID.
+ * Null should be returned if such an identity cannot be found
+ * or the identity is not in an active state (disabled, deleted, etc.)
+ */
+ public static function findIdentity($id);
+ /**
* Returns an ID that can uniquely identify a user identity.
- * The returned ID can be a string, an integer, or any serializable data.
- * @return mixed an ID that uniquely identifies a user identity.
+ * @return string|integer an ID that uniquely identifies a user identity.
*/
public function getId();
/**
* Returns a key that can be used to check the validity of a given identity ID.
- * The space of such keys should be big and random enough to defeat potential identity attacks.
- * The returned key can be a string, an integer, or any serializable data.
- * @return mixed a key that is used to check the validity of a given identity ID.
+ *
+ * The key should be unique for each individual user, and should be persistent
+ * so that it can be used to check the validity of the user identity.
+ *
+ * The space of such keys should be big enough to defeat potential identity attacks.
+ *
+ * This is required if [[User::enableAutoLogin]] is enabled.
+ * @return string a key that is used to check the validity of a given identity ID.
* @see validateAuthKey()
*/
public function getAuthKey();
/**
* Validates the given auth key.
+ *
+ * This is required if [[User::enableAutoLogin]] is enabled.
* @param string $authKey the given auth key
* @return boolean whether the given auth key is valid.
* @see getAuthKey()
*/
public function validateAuthKey($authKey);
- /**
- * Finds an identity by the given ID.
- * @param mixed $id the ID to be looked for
- * @return Identity the identity object that matches the given ID.
- * Null should be returned if such an identity cannot be found.
- */
- public static function findIdentity($id);
}
\ No newline at end of file
diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php
index 29c8cc8..5a50825 100644
--- a/framework/web/PageCache.php
+++ b/framework/web/PageCache.php
@@ -25,11 +25,6 @@ class PageCache extends ActionFilter
*/
public $varyByRoute = true;
/**
- * @var View the view object that is used to create the fragment cache widget to implement page caching.
- * If not set, the view registered with the application will be used.
- */
- public $view;
- /**
* @var string the application component ID of the [[\yii\caching\Cache|cache]] object.
*/
public $cache = 'cache';
diff --git a/framework/web/Request.php b/framework/web/Request.php
index 093a394..369fa0c 100644
--- a/framework/web/Request.php
+++ b/framework/web/Request.php
@@ -43,8 +43,6 @@ class Request extends \yii\base\Request
*/
public function resolve()
{
- Yii::setAlias('@www', $this->getBaseUrl());
-
$result = Yii::$app->getUrlManager()->parseRequest($this);
if ($result !== false) {
list ($route, $params) = $result;
@@ -301,7 +299,8 @@ class Request extends \yii\base\Request
public function getScriptUrl()
{
if ($this->_scriptUrl === null) {
- $scriptName = basename($_SERVER['SCRIPT_FILENAME']);
+ $scriptFile = $this->getScriptFile();
+ $scriptName = basename($scriptFile);
if (basename($_SERVER['SCRIPT_NAME']) === $scriptName) {
$this->_scriptUrl = $_SERVER['SCRIPT_NAME'];
} elseif (basename($_SERVER['PHP_SELF']) === $scriptName) {
@@ -310,8 +309,8 @@ class Request extends \yii\base\Request
$this->_scriptUrl = $_SERVER['ORIG_SCRIPT_NAME'];
} elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $scriptName)) !== false) {
$this->_scriptUrl = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $scriptName;
- } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']) === 0) {
- $this->_scriptUrl = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME']));
+ } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($scriptFile, $_SERVER['DOCUMENT_ROOT']) === 0) {
+ $this->_scriptUrl = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $scriptFile));
} else {
throw new InvalidConfigException('Unable to determine the entry script URL.');
}
@@ -330,6 +329,30 @@ class Request extends \yii\base\Request
$this->_scriptUrl = '/' . trim($value, '/');
}
+ private $_scriptFile;
+
+ /**
+ * Returns the entry script file path.
+ * The default implementation will simply return `$_SERVER['SCRIPT_FILENAME']`.
+ * @return string the entry script file path
+ */
+ public function getScriptFile()
+ {
+ return isset($this->_scriptFile) ? $this->_scriptFile : $_SERVER['SCRIPT_FILENAME'];
+ }
+
+ /**
+ * Sets the entry script file path.
+ * The entry script file path normally can be obtained from `$_SERVER['SCRIPT_FILENAME']`.
+ * If your server configuration does not return the correct value, you may configure
+ * this property to make it right.
+ * @param string $value the entry script file path.
+ */
+ public function setScriptFile($value)
+ {
+ $this->_scriptFile = $value;
+ }
+
private $_pathInfo;
/**
diff --git a/framework/web/Response.php b/framework/web/Response.php
index d23c5b9..1d604e9 100644
--- a/framework/web/Response.php
+++ b/framework/web/Response.php
@@ -9,6 +9,7 @@ namespace yii\web;
use Yii;
use yii\helpers\FileHelper;
+use yii\helpers\Html;
/**
* @author Qiang Xue
@@ -17,6 +18,14 @@ use yii\helpers\FileHelper;
class Response extends \yii\base\Response
{
/**
+ * @var integer the HTTP status code that should be used when redirecting in AJAX mode.
+ * This is used by [[redirect()]]. A 2xx code should normally be used for this purpose
+ * so that the AJAX handler will treat the response as a success.
+ * @see redirect
+ */
+ public $ajaxRedirectCode = 278;
+
+ /**
* Sends a file to user.
* @param string $fileName file name
* @param string $content content to be set.
@@ -103,61 +112,89 @@ class Response extends \yii\base\Response
*
mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.
*
xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"
*
terminate: whether to terminate the current application after calling this method, defaults to true
- *
forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true. (Since version 1.1.9.)
- *
addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)
+ *
forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true
+ *
addHeaders: an array of additional http headers in header-value pairs
*
*/
- public function xSendFile($filePath, $options=array())
+ public function xSendFile($filePath, $options = array())
{
- if(!isset($options['forceDownload']) || $options['forceDownload'])
- $disposition='attachment';
- else
- $disposition='inline';
+ if (!isset($options['forceDownload']) || $options['forceDownload']) {
+ $disposition = 'attachment';
+ } else {
+ $disposition = 'inline';
+ }
- if(!isset($options['saveName']))
- $options['saveName']=basename($filePath);
+ if (!isset($options['saveName'])) {
+ $options['saveName'] = basename($filePath);
+ }
- if(!isset($options['mimeType']))
- {
- if(($options['mimeType']=CFileHelper::getMimeTypeByExtension($filePath))===null)
- $options['mimeType']='text/plain';
+ if (!isset($options['mimeType'])) {
+ if (($options['mimeType'] = CFileHelper::getMimeTypeByExtension($filePath)) === null) {
+ $options['mimeType'] = 'text/plain';
+ }
}
- if(!isset($options['xHeader']))
- $options['xHeader']='X-Sendfile';
+ if (!isset($options['xHeader'])) {
+ $options['xHeader'] = 'X-Sendfile';
+ }
- if($options['mimeType'] !== null)
- header('Content-type: '.$options['mimeType']);
- header('Content-Disposition: '.$disposition.'; filename="'.$options['saveName'].'"');
- if(isset($options['addHeaders']))
- {
- foreach($options['addHeaders'] as $header=>$value)
- header($header.': '.$value);
+ if ($options['mimeType'] !== null) {
+ header('Content-type: ' . $options['mimeType']);
}
- header(trim($options['xHeader']).': '.$filePath);
+ header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"');
+ if (isset($options['addHeaders'])) {
+ foreach ($options['addHeaders'] as $header => $value) {
+ header($header . ': ' . $value);
+ }
+ }
+ header(trim($options['xHeader']) . ': ' . $filePath);
- if(!isset($options['terminate']) || $options['terminate'])
- Yii::app()->end();
+ if (!isset($options['terminate']) || $options['terminate']) {
+ Yii::$app->end();
+ }
}
/**
* Redirects the browser to the specified URL.
- * @param string $url URL to be redirected to. Note that when URL is not
- * absolute (not starting with "/") it will be relative to current request URL.
+ * This method will send out a "Location" header to achieve the redirection.
+ * In AJAX mode, this normally will not work as expected unless there are some
+ * client-side JavaScript code handling the redirection. To help achieve this goal,
+ * this method will use [[ajaxRedirectCode]] as the HTTP status code when performing
+ * redirection in AJAX mode. The following JavaScript code may be used on the client
+ * side to handle the redirection response:
+ *
+ * ~~~
+ * $(document).ajaxSuccess(function(event, xhr, settings) {
+ * if (xhr.status == 278) {
+ * window.location = xhr.getResponseHeader('Location');
+ * }
+ * });
+ * ~~~
+ *
+ * @param array|string $url the URL to be redirected to. [[\yii\helpers\Html::url()]]
+ * will be used to normalize the URL. If the resulting URL is still a relative URL
+ * (one without host info), the current request host info will be used.
* @param boolean $terminate whether to terminate the current application
- * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html}
+ * @param integer $statusCode the HTTP status code. Defaults to 302.
+ * See [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html]]
* for details about HTTP status code.
+ * Note that if the request is an AJAX request, [[ajaxRedirectCode]] will be used instead.
*/
- public function redirect($url,$terminate=true,$statusCode=302)
+ public function redirect($url, $terminate = true, $statusCode = 302)
{
- if(strpos($url,'/')===0 && strpos($url,'//')!==0)
- $url=$this->getHostInfo().$url;
- header('Location: '.$url, true, $statusCode);
- if($terminate)
- Yii::app()->end();
+ $url = Html::url($url);
+ if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
+ $url = Yii::$app->getRequest()->getHostInfo() . $url;
+ }
+ if (Yii::$app->getRequest()->getIsAjaxRequest()) {
+ $statusCode = $this->ajaxRedirectCode;
+ }
+ header('Location: ' . $url, true, $statusCode);
+ if ($terminate) {
+ Yii::$app->end();
+ }
}
-
/**
* Returns the cookie collection.
* Through the returned cookie collection, you add or remove cookies as follows,
diff --git a/framework/web/Session.php b/framework/web/Session.php
index c289db2..4c0505f 100644
--- a/framework/web/Session.php
+++ b/framework/web/Session.php
@@ -60,6 +60,13 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
public $flashVar = '__flash';
/**
+ * @var array parameter-value pairs to override default session cookie parameters
+ */
+ public $cookieParams = array(
+ 'httponly' => true
+ );
+
+ /**
* Initializes the application component.
* This method is required by IApplicationComponent and is invoked by application.
*/
@@ -111,13 +118,15 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
);
}
+ $this->setCookieParams($this->cookieParams);
+
@session_start();
if (session_id() == '') {
$this->_opened = false;
$error = error_get_last();
$message = isset($error['message']) ? $error['message'] : 'Failed to start session.';
- Yii::error($message, __CLASS__);
+ Yii::error($message, __METHOD__);
} else {
$this->_opened = true;
$this->updateFlashCounters();
diff --git a/framework/web/Sort.php b/framework/web/Sort.php
index 7cfeeca..e5c2451 100644
--- a/framework/web/Sort.php
+++ b/framework/web/Sort.php
@@ -216,7 +216,7 @@ class Sort extends \yii\base\Object
$url = $this->createUrl($attribute);
- return Html::link($label, $url, $htmlOptions);
+ return Html::a($label, $url, $htmlOptions);
}
private $_attributeOrders;
diff --git a/framework/web/UploadedFile.php b/framework/web/UploadedFile.php
new file mode 100644
index 0000000..c67281c
--- /dev/null
+++ b/framework/web/UploadedFile.php
@@ -0,0 +1,246 @@
+
+ * @since 2.0
+ */
+class UploadedFile extends \yii\base\Object
+{
+ private static $_files;
+ private $_name;
+ private $_tempName;
+ private $_type;
+ private $_size;
+ private $_error;
+
+
+ /**
+ * Constructor.
+ * Instead of using the constructor to create a new instance,
+ * you should normally call [[getInstance()]] or [[getInstances()]]
+ * to obtain new instances.
+ * @param string $name the original name of the file being uploaded
+ * @param string $tempName the path of the uploaded file on the server.
+ * @param string $type the MIME-type of the uploaded file (such as "image/gif").
+ * @param integer $size the actual size of the uploaded file in bytes
+ * @param integer $error the error code
+ */
+ public function __construct($name, $tempName, $type, $size, $error)
+ {
+ $this->_name = $name;
+ $this->_tempName = $tempName;
+ $this->_type = $type;
+ $this->_size = $size;
+ $this->_error = $error;
+ }
+
+ /**
+ * String output.
+ * This is PHP magic method that returns string representation of an object.
+ * The implementation here returns the uploaded file's name.
+ * @return string the string representation of the object
+ */
+ public function __toString()
+ {
+ return $this->_name;
+ }
+
+ /**
+ * Returns an uploaded file for the given model attribute.
+ * The file should be uploaded using [[ActiveForm::fileInput()]].
+ * @param \yii\base\Model $model the data model
+ * @param string $attribute the attribute name. The attribute name may contain array indexes.
+ * For example, '[1]file' for tabular file uploading; and 'file[1]' for an element in a file array.
+ * @return UploadedFile the instance of the uploaded file.
+ * Null is returned if no file is uploaded for the specified model attribute.
+ * @see getInstanceByName
+ */
+ public static function getInstance($model, $attribute)
+ {
+ $name = ActiveForm::getInputName($model, $attribute);
+ return static::getInstanceByName($name);
+ }
+
+ /**
+ * Returns all uploaded files for the given model attribute.
+ * @param \yii\base\Model $model the data model
+ * @param string $attribute the attribute name. The attribute name may contain array indexes
+ * for tabular file uploading, e.g. '[1]file'.
+ * @return UploadedFile[] array of UploadedFile objects.
+ * Empty array is returned if no available file was found for the given attribute.
+ */
+ public static function getInstances($model, $attribute)
+ {
+ $name = ActiveForm::getInputName($model, $attribute);
+ return static::getInstancesByName($name);
+ }
+
+ /**
+ * Returns an uploaded file according to the given file input name.
+ * The name can be a plain string or a string like an array element (e.g. 'Post[imageFile]', or 'Post[0][imageFile]').
+ * @param string $name the name of the file input field.
+ * @return UploadedFile the instance of the uploaded file.
+ * Null is returned if no file is uploaded for the specified name.
+ */
+ public static function getInstanceByName($name)
+ {
+ $files = static::loadFiles();
+ return isset($files[$name]) ? $files[$name] : null;
+ }
+
+ /**
+ * Returns an array of uploaded files corresponding to the specified file input name.
+ * This is mainly used when multiple files were uploaded and saved as 'files[0]', 'files[1]',
+ * 'files[n]'..., and you can retrieve them all by passing 'files' as the name.
+ * @param string $name the name of the array of files
+ * @return UploadedFile[] the array of CUploadedFile objects. Empty array is returned
+ * if no adequate upload was found. Please note that this array will contain
+ * all files from all sub-arrays regardless how deeply nested they are.
+ */
+ public static function getInstancesByName($name)
+ {
+ $files = static::loadFiles();
+ if (isset($files[$name])) {
+ return array($files[$name]);
+ }
+ $results = array();
+ foreach ($files as $key => $file) {
+ if (strpos($key, "{$name}[") === 0) {
+ $results[] = self::$_files[$key];
+ }
+ }
+ return $results;
+ }
+
+ /**
+ * Cleans up the loaded UploadedFile instances.
+ * This method is mainly used by test scripts to set up a fixture.
+ */
+ public static function reset()
+ {
+ self::$_files = null;
+ }
+
+ /**
+ * Saves the uploaded file.
+ * Note that this method uses php's move_uploaded_file() method. If the target file `$file`
+ * already exists, it will be overwritten.
+ * @param string $file the file path used to save the uploaded file
+ * @param boolean $deleteTempFile whether to delete the temporary file after saving.
+ * If true, you will not be able to save the uploaded file again in the current request.
+ * @return boolean true whether the file is saved successfully
+ * @see error
+ */
+ public function saveAs($file, $deleteTempFile = true)
+ {
+ if ($this->_error == UPLOAD_ERR_OK) {
+ if ($deleteTempFile) {
+ return move_uploaded_file($this->_tempName, $file);
+ } elseif (is_uploaded_file($this->_tempName)) {
+ return copy($this->_tempName, $file);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return string the original name of the file being uploaded
+ */
+ public function getName()
+ {
+ return $this->_name;
+ }
+
+ /**
+ * @return string the path of the uploaded file on the server.
+ * Note, this is a temporary file which will be automatically deleted by PHP
+ * after the current request is processed.
+ */
+ public function getTempName()
+ {
+ return $this->_tempName;
+ }
+
+ /**
+ * @return string the MIME-type of the uploaded file (such as "image/gif").
+ * Since this MIME type is not checked on the server side, do not take this value for granted.
+ * Instead, use [[FileHelper::getMimeType()]] to determine the exact MIME type.
+ */
+ public function getType()
+ {
+ return $this->_type;
+ }
+
+ /**
+ * @return integer the actual size of the uploaded file in bytes
+ */
+ public function getSize()
+ {
+ return $this->_size;
+ }
+
+ /**
+ * Returns an error code describing the status of this file uploading.
+ * @return integer the error code
+ * @see http://www.php.net/manual/en/features.file-upload.errors.php
+ */
+ public function getError()
+ {
+ return $this->_error;
+ }
+
+ /**
+ * @return boolean whether there is an error with the uploaded file.
+ * Check [[error]] for detailed error code information.
+ */
+ public function getHasError()
+ {
+ return $this->_error != UPLOAD_ERR_OK;
+ }
+
+ /**
+ * Creates UploadedFile instances from $_FILE.
+ * @return array the UploadedFile instances
+ */
+ private static function loadFiles()
+ {
+ if (self::$_files === null) {
+ self::$_files = array();
+ if (isset($_FILES) && is_array($_FILES)) {
+ foreach ($_FILES as $class => $info) {
+ self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']);
+ }
+ }
+ }
+ return self::$_files;
+ }
+
+ /**
+ * Creates UploadedFile instances from $_FILE recursively.
+ * @param string $key key for identifying uploaded file: class name and sub-array indexes
+ * @param mixed $names file names provided by PHP
+ * @param mixed $tempNames temporary file names provided by PHP
+ * @param mixed $types file types provided by PHP
+ * @param mixed $sizes file sizes provided by PHP
+ * @param mixed $errors uploading issues provided by PHP
+ */
+ private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors)
+ {
+ if (is_array($names)) {
+ foreach ($names as $i => $name) {
+ self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]);
+ }
+ } else {
+ self::$_files[$key] = new self($names, $tempNames, $types, $sizes, $errors);
+ }
+ }
+}
diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php
index 459e8e8..755d644 100644
--- a/framework/web/UrlManager.php
+++ b/framework/web/UrlManager.php
@@ -74,9 +74,6 @@ class UrlManager extends Component
public function init()
{
parent::init();
- if (is_string($this->cache)) {
- $this->cache = Yii::$app->getComponent($this->cache);
- }
$this->compileRules();
}
@@ -88,6 +85,9 @@ class UrlManager extends Component
if (!$this->enablePrettyUrl || $this->rules === array()) {
return;
}
+ if (is_string($this->cache)) {
+ $this->cache = Yii::$app->getComponent($this->cache);
+ }
if ($this->cache instanceof Cache) {
$key = $this->cache->buildKey(__CLASS__);
$hash = md5(json_encode($this->rules));
@@ -104,7 +104,7 @@ class UrlManager extends Component
$this->rules[$i] = Yii::createObject($rule);
}
- if ($this->cache instanceof Cache) {
+ if (isset($key, $hash)) {
$this->cache->set($key, array($this->rules, $hash));
}
}
diff --git a/framework/web/User.php b/framework/web/User.php
index 2326a10..435b606 100644
--- a/framework/web/User.php
+++ b/framework/web/User.php
@@ -9,24 +9,30 @@ namespace yii\web;
use Yii;
use yii\base\Component;
+use yii\base\HttpException;
use yii\base\InvalidConfigException;
/**
+ * User is the class for the "user" application component that manages the user authentication status.
+ *
+ * In particular, [[User::isGuest]] returns a value indicating whether the current user is a guest or not.
+ * Through methods [[login()]] and [[logout()]], you can change the user authentication status.
+ *
+ * User works with a class implementing the [[Identity]] interface. This class implements
+ * the actual user authentication logic and is often backed by a user database table.
+ *
* @author Qiang Xue
* @since 2.0
*/
class User extends Component
{
- const ID_VAR = '__id';
- const AUTH_EXPIRE_VAR = '__expire';
-
const EVENT_BEFORE_LOGIN = 'beforeLogin';
const EVENT_AFTER_LOGIN = 'afterLogin';
const EVENT_BEFORE_LOGOUT = 'beforeLogout';
const EVENT_AFTER_LOGOUT = 'afterLogout';
/**
- * @var string the class name of the [[identity]] object.
+ * @var string the class name or alias of the [[identity]] object.
*/
public $identityClass;
/**
@@ -50,7 +56,7 @@ class User extends Component
* @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true.
* @see Cookie
*/
- public $identityCookie = array('name' => '__identity');
+ public $identityCookie = array('name' => '__identity', 'httponly' => true);
/**
* @var integer the number of seconds in which the user will be logged out automatically if he
* remains inactive. If this property is not set, the user will be logged out after
@@ -59,29 +65,27 @@ class User extends Component
public $authTimeout;
/**
* @var boolean whether to automatically renew the identity cookie each time a page is requested.
- * Defaults to false. This property is effective only when {@link enableAutoLogin} is true.
+ * This property is effective only when [[enableAutoLogin]] is true.
* When this is false, the identity cookie will expire after the specified duration since the user
* is initially logged in. When this is true, the identity cookie will expire after the specified duration
* since the user visits the site the last time.
* @see enableAutoLogin
- * @since 1.1.0
*/
- public $autoRenewCookie = false;
+ public $autoRenewCookie = true;
/**
- * @var string value that will be echoed in case that user session has expired during an ajax call.
- * When a request is made and user session has expired, {@link loginRequired} redirects to {@link loginUrl} for login.
- * If that happens during an ajax call, the complete HTML login page is returned as the result of that ajax call. That could be
- * a problem if the ajax call expects the result to be a json array or a predefined string, as the login page is ignored in that case.
- * To solve this, set this property to the desired return value.
- *
- * If this property is set, this value will be returned as the result of the ajax call in case that the user session has expired.
- * @since 1.1.9
- * @see loginRequired
+ * @var string the session variable name used to store the value of [[id]].
+ */
+ public $idVar = '__id';
+ /**
+ * @var string the session variable name used to store the value of expiration timestamp of the authenticated state.
+ * This is used when [[authTimeout]] is set.
*/
- public $loginRequiredAjaxResponse;
-
+ public $authTimeoutVar = '__expire';
+ /**
+ * @var string the session variable name used to store the value of [[returnUrl]].
+ */
+ public $returnUrlVar = '__returnUrl';
- public $stateVar = '__states';
/**
* Initializes the application component.
@@ -90,6 +94,9 @@ class User extends Component
{
parent::init();
+ if ($this->identityClass === null) {
+ throw new InvalidConfigException('User::identityClass must be set.');
+ }
if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) {
throw new InvalidConfigException('User::identityCookie must contain the "name" element.');
}
@@ -107,11 +114,15 @@ class User extends Component
}
}
- /**
- * @var Identity the identity object associated with the currently logged user.
- */
private $_identity = false;
+ /**
+ * Returns the identity object associated with the currently logged user.
+ * @return Identity the identity object associated with the currently logged user.
+ * Null is returned if the user is not logged in (not authenticated).
+ * @see login
+ * @see logout
+ */
public function getIdentity()
{
if ($this->_identity === false) {
@@ -120,53 +131,56 @@ class User extends Component
$this->_identity = null;
} else {
/** @var $class Identity */
- $class = $this->identityClass;
- $this->_identity = $class::findIdentity($this->getId());
+ $class = Yii::import($this->identityClass);
+ $this->_identity = $class::findIdentity($id);
}
}
return $this->_identity;
}
+ /**
+ * Sets the identity object.
+ * This method should be mainly be used by the User component or its child class
+ * to maintain the identity object.
+ *
+ * You should normally update the user identity via methods [[login()]], [[logout()]]
+ * or [[switchIdentity()]].
+ *
+ * @param Identity $identity the identity object associated with the currently logged user.
+ */
public function setIdentity($identity)
{
- $this->switchIdentity($identity);
+ $this->_identity = $identity;
}
/**
* Logs in a user.
*
- * The user identity information will be saved in storage that is
- * persistent during the user session. By default, the storage is simply
- * the session storage. If the duration parameter is greater than 0,
- * a cookie will be sent to prepare for cookie-based login in future.
- *
- * Note, you have to set {@link enableAutoLogin} to true
- * if you want to allow user to be authenticated based on the cookie information.
+ * This method stores the necessary session information to keep track
+ * of the user identity information. If `$duration` is greater than 0
+ * and [[enableAutoLogin]] is true, it will also send out an identity
+ * cookie to support cookie-based login.
*
* @param Identity $identity the user identity (which should already be authenticated)
- * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser.
- * If greater than 0, cookie-based login will be used. In this case, {@link enableAutoLogin}
- * must be set true, otherwise an exception will be thrown.
+ * @param integer $duration number of seconds that the user can remain in logged-in status.
+ * Defaults to 0, meaning login till the user closes the browser or the session is manually destroyed.
+ * If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported.
* @return boolean whether the user is logged in
*/
public function login($identity, $duration = 0)
{
if ($this->beforeLogin($identity, false)) {
- $this->switchIdentity($identity);
- if ($duration > 0 && $this->enableAutoLogin) {
- $this->saveIdentityCookie($identity, $duration);
- }
+ $this->switchIdentity($identity, $duration);
$this->afterLogin($identity, false);
}
return !$this->getIsGuest();
}
/**
- * Populates the current user object with the information obtained from cookie.
- * This method is used when automatic login ({@link enableAutoLogin}) is enabled.
- * The user identity information is recovered from cookie.
- * Sufficient security measures are used to prevent cookie data from being tampered.
- * @see saveIdentityCookie
+ * Logs in a user by cookie.
+ *
+ * This method attempts to log in a user using the ID and authKey information
+ * provided by the given cookie.
*/
protected function loginByCookie()
{
@@ -179,12 +193,13 @@ class User extends Component
/** @var $class Identity */
$class = $this->identityClass;
$identity = $class::findIdentity($id);
- if ($identity !== null && $identity->validateAuthKey($authKey) && $this->beforeLogin($identity, true)) {
- $this->switchIdentity($identity);
- if ($this->autoRenewCookie) {
- $this->saveIdentityCookie($identity, $duration);
+ if ($identity !== null && $identity->validateAuthKey($authKey)) {
+ if ($this->beforeLogin($identity, true)) {
+ $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);
+ $this->afterLogin($identity, true);
}
- $this->afterLogin($identity, true);
+ } elseif ($identity !== null) {
+ Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__);
}
}
}
@@ -193,18 +208,14 @@ class User extends Component
/**
* Logs out the current user.
* This will remove authentication-related session data.
- * If the parameter is true, the whole session will be destroyed as well.
- * @param boolean $destroySession whether to destroy the whole session. Defaults to true. If false,
- * then {@link clearStates} will be called, which removes only the data stored via {@link setState}.
+ * If `$destroySession` is true, all session data will be removed.
+ * @param boolean $destroySession whether to destroy the whole session. Defaults to true.
*/
public function logout($destroySession = true)
{
$identity = $this->getIdentity();
if ($identity !== null && $this->beforeLogout($identity)) {
$this->switchIdentity(null);
- if ($this->enableAutoLogin) {
- Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie));
- }
if ($destroySession) {
Yii::$app->getSession()->destroy();
}
@@ -223,97 +234,74 @@ class User extends Component
/**
* Returns a value that uniquely represents the user.
- * @return mixed the unique identifier for the user. If null, it means the user is a guest.
+ * @return string|integer the unique identifier for the user. If null, it means the user is a guest.
*/
public function getId()
{
- return $this->getState(static::ID_VAR);
- }
-
- /**
- * @param mixed $value the unique identifier for the user. If null, it means the user is a guest.
- */
- public function setId($value)
- {
- $this->setState(static::ID_VAR, $value);
+ return Yii::$app->getSession()->get($this->idVar);
}
/**
* Returns the URL that the user should be redirected to after successful login.
* This property is usually used by the login action. If the login is successful,
* the action should read this property and use it to redirect the user browser.
- * @param string $defaultUrl the default return URL in case it was not set previously. If this is null,
- * the application entry URL will be considered as the default return URL.
+ * @param string|array $defaultUrl the default return URL in case it was not set previously.
+ * If this is null, it means [[Application::homeUrl]] will be redirected to.
+ * Please refer to [[\yii\helpers\Html::url()]] on acceptable URL formats.
* @return string the URL that the user should be redirected to after login.
* @see loginRequired
*/
public function getReturnUrl($defaultUrl = null)
{
- if ($defaultUrl === null) {
- $defaultReturnUrl = Yii::app()->getUrlManager()->showScriptName ? Yii::app()->getRequest()->getScriptUrl() : Yii::app()->getRequest()->getBaseUrl() . '/';
- } else {
- $defaultReturnUrl = CHtml::normalizeUrl($defaultUrl);
- }
- return $this->getState('__returnUrl', $defaultReturnUrl);
+ $url = Yii::$app->getSession()->get($this->returnUrlVar, $defaultUrl);
+ return $url === null ? Yii::$app->getHomeUrl() : $url;
}
/**
- * @param string $value the URL that the user should be redirected to after login.
+ * @param string|array $url the URL that the user should be redirected to after login.
+ * Please refer to [[\yii\helpers\Html::url()]] on acceptable URL formats.
*/
- public function setReturnUrl($value)
+ public function setReturnUrl($url)
{
- $this->setState('__returnUrl', $value);
+ Yii::$app->getSession()->set($this->returnUrlVar, $url);
}
/**
* Redirects the user browser to the login page.
* Before the redirection, the current URL (if it's not an AJAX url) will be
- * kept in {@link returnUrl} so that the user browser may be redirected back
- * to the current page after successful login. Make sure you set {@link loginUrl}
+ * kept as [[returnUrl]] so that the user browser may be redirected back
+ * to the current page after successful login. Make sure you set [[loginUrl]]
* so that the user browser can be redirected to the specified login URL after
* calling this method.
* After calling this method, the current request processing will be terminated.
*/
public function loginRequired()
{
- $app = Yii::app();
- $request = $app->getRequest();
-
+ $request = Yii::$app->getRequest();
if (!$request->getIsAjaxRequest()) {
$this->setReturnUrl($request->getUrl());
- } elseif (isset($this->loginRequiredAjaxResponse)) {
- echo $this->loginRequiredAjaxResponse;
- Yii::app()->end();
}
-
- if (($url = $this->loginUrl) !== null) {
- if (is_array($url)) {
- $route = isset($url[0]) ? $url[0] : $app->defaultController;
- $url = $app->createUrl($route, array_splice($url, 1));
- }
- $request->redirect($url);
+ if ($this->loginUrl !== null) {
+ Yii::$app->getResponse()->redirect($this->loginUrl);
} else {
- throw new CHttpException(403, Yii::t('yii', 'Login Required'));
+ throw new HttpException(403, Yii::t('yii|Login Required'));
}
}
/**
* This method is called before logging in a user.
- * You may override this method to provide additional security check.
- * For example, when the login is cookie-based, you may want to verify
- * that the user ID together with a random token in the states can be found
- * in the database. This will prevent hackers from faking arbitrary
- * identity cookies even if they crack down the server private key.
- * @param mixed $id the user ID. This is the same as returned by {@link getId()}.
- * @param array $states a set of name-value pairs that are provided by the user identity.
- * @param boolean $fromCookie whether the login is based on cookie
- * @return boolean whether the user should be logged in
- */
- protected function beforeLogin($identity, $fromCookie)
+ * The default implementation will trigger the [[EVENT_BEFORE_LOGIN]] event.
+ * If you override this method, make sure you call the parent implementation
+ * so that the event is triggered.
+ * @param Identity $identity the user identity information
+ * @param boolean $cookieBased whether the login is cookie-based
+ * @return boolean whether the user should continue to be logged in
+ */
+ protected function beforeLogin($identity, $cookieBased)
{
$event = new UserEvent(array(
'identity' => $identity,
- 'fromCookie' => $fromCookie,
+ 'cookieBased' => $cookieBased,
));
$this->trigger(self::EVENT_BEFORE_LOGIN, $event);
return $event->isValid;
@@ -321,24 +309,27 @@ class User extends Component
/**
* This method is called after the user is successfully logged in.
- * You may override this method to do some postprocessing (e.g. log the user
- * login IP and time; load the user permission information).
- * @param boolean $fromCookie whether the login is based on cookie.
+ * The default implementation will trigger the [[EVENT_AFTER_LOGIN]] event.
+ * If you override this method, make sure you call the parent implementation
+ * so that the event is triggered.
+ * @param Identity $identity the user identity information
+ * @param boolean $cookieBased whether the login is cookie-based
*/
- protected function afterLogin($identity, $fromCookie)
+ protected function afterLogin($identity, $cookieBased)
{
$this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent(array(
'identity' => $identity,
- 'fromCookie' => $fromCookie,
+ 'cookieBased' => $cookieBased,
)));
}
/**
- * This method is invoked when calling {@link logout} to log out a user.
- * If this method return false, the logout action will be cancelled.
- * You may override this method to provide additional check before
- * logging out a user.
- * @return boolean whether to log out the user
+ * This method is invoked when calling [[logout()]] to log out a user.
+ * The default implementation will trigger the [[EVENT_BEFORE_LOGOUT]] event.
+ * If you override this method, make sure you call the parent implementation
+ * so that the event is triggered.
+ * @param Identity $identity the user identity information
+ * @return boolean whether the user should continue to be logged out
*/
protected function beforeLogout($identity)
{
@@ -350,8 +341,11 @@ class User extends Component
}
/**
- * This method is invoked right after a user is logged out.
- * You may override this method to do some extra cleanup work for the user.
+ * This method is invoked right after a user is logged out via [[logout()]].
+ * The default implementation will trigger the [[EVENT_AFTER_LOGOUT]] event.
+ * If you override this method, make sure you call the parent implementation
+ * so that the event is triggered.
+ * @param Identity $identity the user identity information
*/
protected function afterLogout($identity)
{
@@ -360,7 +354,6 @@ class User extends Component
)));
}
-
/**
* Renews the identity cookie.
* This method will set the expiration time of the identity cookie to be the current time
@@ -382,15 +375,15 @@ class User extends Component
}
/**
- * Saves necessary user data into a cookie.
- * This method is used when automatic login ({@link enableAutoLogin}) is enabled.
- * This method saves user ID, username, other identity states and a validation key to cookie.
- * These information are used to do authentication next time when user visits the application.
+ * Sends an identity cookie.
+ * This method is used when [[enableAutoLogin]] is true.
+ * It saves [[id]], [[Identity::getAuthKey()|auth key]], and the duration of cookie-based login
+ * information in the cookie.
* @param Identity $identity
- * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser.
+ * @param integer $duration number of seconds that the user can remain in logged-in status.
* @see loginByCookie
*/
- protected function saveIdentityCookie($identity, $duration)
+ protected function sendIdentityCookie($identity, $duration)
{
$cookie = new Cookie($this->identityCookie);
$cookie->value = json_encode(array(
@@ -403,145 +396,55 @@ class User extends Component
}
/**
- * Changes the current user with the specified identity information.
- * This method is called by {@link login} and {@link restoreFromCookie}
- * when the current user needs to be populated with the corresponding
- * identity information. Derived classes may override this method
- * by retrieving additional user-related information. Make sure the
- * parent implementation is called first.
- * @param Identity $identity a unique identifier for the user
+ * Switches to a new identity for the current user.
+ *
+ * This method will save necessary session information to keep track of the user authentication status.
+ * If `$duration` is provided, it will also send out appropriate identity cookie
+ * to support cookie-based login.
+ *
+ * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]]
+ * when the current user needs to be associated with the corresponding identity information.
+ *
+ * @param Identity $identity the identity information to be associated with the current user.
+ * If null, it means switching to be a guest.
+ * @param integer $duration number of seconds that the user can remain in logged-in status.
+ * This parameter is used only when `$identity` is not null.
*/
- protected function switchIdentity($identity)
+ public function switchIdentity($identity, $duration = 0)
{
- Yii::$app->getSession()->regenerateID(true);
+ $session = Yii::$app->getSession();
+ $session->regenerateID(true);
$this->setIdentity($identity);
+ $session->remove($this->idVar);
+ $session->remove($this->authTimeoutVar);
if ($identity instanceof Identity) {
- $this->setId($identity->getId());
+ $session->set($this->idVar, $identity->getId());
if ($this->authTimeout !== null) {
- $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout);
+ $session->set($this->authTimeoutVar, time() + $this->authTimeout);
}
- } else {
- $this->removeAllStates();
+ if ($duration > 0 && $this->enableAutoLogin) {
+ $this->sendIdentityCookie($identity, $duration);
+ }
+ } elseif ($this->enableAutoLogin) {
+ Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie));
}
}
/**
- * Updates the authentication status according to {@link authTimeout}.
- * If the user has been inactive for {@link authTimeout} seconds,
- * he will be automatically logged out.
+ * Updates the authentication status according to [[authTimeout]].
+ * This method is called during [[init()]].
+ * It will update the user's authentication status if it has not outdated yet.
+ * Otherwise, it will logout the user.
*/
protected function renewAuthStatus()
{
if ($this->authTimeout !== null && !$this->getIsGuest()) {
- $expire = $this->getState(self::AUTH_EXPIRE_VAR);
+ $expire = Yii::$app->getSession()->get($this->authTimeoutVar);
if ($expire !== null && $expire < time()) {
$this->logout(false);
} else {
- $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout);
- }
- }
- }
-
- /**
- * Returns a user state.
- * A user state is a session data item associated with the current user.
- * If the user logs out, all his/her user states will be removed.
- * @param string $key the key identifying the state
- * @param mixed $defaultValue value to be returned if the state does not exist.
- * @return mixed the state
- */
- public function getState($key, $defaultValue = null)
- {
- $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null;
- if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) {
- return $_SESSION[$key];
- } else {
- return $defaultValue;
- }
- }
-
- /**
- * Returns all user states.
- * @return array states (key => state).
- */
- public function getAllStates()
- {
- $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null;
- $states = array();
- if (is_array($manifest)) {
- foreach (array_keys($manifest) as $key) {
- if (isset($_SESSION[$key])) {
- $states[$key] = $_SESSION[$key];
- }
+ Yii::$app->getSession()->set($this->authTimeoutVar, time() + $this->authTimeout);
}
}
- return $states;
- }
-
- /**
- * Stores a user state.
- * A user state is a session data item associated with the current user.
- * If the user logs out, all his/her user states will be removed.
- * @param string $key the key identifying the state. Note that states
- * and normal session variables share the same name space. If you have a normal
- * session variable using the same name, its value will be overwritten by this method.
- * @param mixed $value state
- */
- public function setState($key, $value)
- {
- $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : array();
- $manifest[$value] = true;
- $_SESSION[$key] = $value;
- $_SESSION[$this->stateVar] = $manifest;
- }
-
- /**
- * Removes a user state.
- * If the user logs out, all his/her user states will be removed automatically.
- * @param string $key the key identifying the state. Note that states
- * and normal session variables share the same name space. If you have a normal
- * session variable using the same name, it will be removed by this method.
- * @return mixed the removed state. Null if the state does not exist.
- */
- public function removeState($key)
- {
- $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null;
- if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) {
- $value = $_SESSION[$key];
- } else {
- $value = null;
- }
- unset($_SESSION[$this->stateVar][$key], $_SESSION[$key]);
- return $value;
- }
-
- /**
- * Removes all states.
- * If the user logs out, all his/her user states will be removed automatically
- * without the need to call this method manually.
- *
- * Note that states and normal session variables share the same name space.
- * If you have a normal session variable using the same name, it will be removed
- * by this method.
- */
- public function removeAllStates()
- {
- $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null;
- if (is_array($manifest)) {
- foreach (array_keys($manifest) as $key) {
- unset($_SESSION[$key]);
- }
- }
- unset($_SESSION[$this->stateVar]);
- }
-
- /**
- * Returns a value indicating whether there is a state associated with the specified key.
- * @param string $key key identifying the state
- * @return boolean whether the specified state exists
- */
- public function hasState($key)
- {
- return $this->getState($key) !== null;
}
}
diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php
index 3a8723a..7a5d23d 100644
--- a/framework/web/UserEvent.php
+++ b/framework/web/UserEvent.php
@@ -24,11 +24,11 @@ class UserEvent extends Event
* @var boolean whether the login is cookie-based. This property is only meaningful
* for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_AFTER_LOGIN]] events.
*/
- public $fromCookie;
+ public $cookieBased;
/**
* @var boolean whether the login or logout should proceed.
* Event handlers may modify this property to determine whether the login or logout should proceed.
* This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events.
*/
- public $isValid;
+ public $isValid = true;
}
\ No newline at end of file
diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php
index 2c965e7..83506dd 100644
--- a/framework/widgets/ActiveForm.php
+++ b/framework/widgets/ActiveForm.php
@@ -39,10 +39,16 @@ class ActiveForm extends Widget
public $errorMessageClass = 'yii-error-message';
/**
* @var string the default CSS class that indicates an input has error.
- * This is
*/
public $errorClass = 'yii-error';
+ /**
+ * @var string the default CSS class that indicates an input validated successfully.
+ */
public $successClass = 'yii-success';
+
+ /**
+ * @var string the default CSS class that indicates an input is currently being validated.
+ */
public $validatingClass = 'yii-validating';
/**
* @var boolean whether to enable client-side data validation. Defaults to false.
@@ -52,10 +58,6 @@ class ActiveForm extends Widget
public $enableClientValidation = false;
public $options = array();
- /**
- * @var array model-class mapped to name prefix
- */
- public $modelMap;
/**
* @param Model|Model[] $models
@@ -68,7 +70,7 @@ class ActiveForm extends Widget
$models = array($models);
}
- $showAll = isset($options['showAll']) && $options['showAll'];
+ $showAll = !empty($options['showAll']);
$lines = array();
/** @var $model Model */
foreach ($models as $model) {
@@ -110,8 +112,7 @@ class ActiveForm extends Widget
*/
public function error($model, $attribute, $options = array())
{
- $attribute = $this->normalizeAttributeName($attribute);
- $this->getInputName($model, $attribute);
+ $attribute = $this->getAttributeName($attribute);
$tag = isset($options['tag']) ? $options['tag'] : 'div';
unset($options['tag']);
$error = $model->getFirstError($attribute);
@@ -126,15 +127,27 @@ class ActiveForm extends Widget
*/
public function label($model, $attribute, $options = array())
{
- $attribute = $this->normalizeAttributeName($attribute);
- $label = $model->getAttributeLabel($attribute);
- return Html::label(Html::encode($label), isset($options['for']) ? $options['for'] : null, $options);
+ $attribute = $this->getAttributeName($attribute);
+ $label = isset($options['label']) ? $options['label'] : Html::encode($model->getAttributeLabel($attribute));
+ $for = array_key_exists('for', $options) ? $options['for'] : $this->getInputId($model, $attribute);
+ return Html::label($label, $for, $options);
}
+ /**
+ * @param string $type
+ * @param Model $model
+ * @param string $attribute
+ * @param array $options
+ *
+ * @return string
+ */
public function input($type, $model, $attribute, $options = array())
{
$value = $this->getAttributeValue($model, $attribute);
$name = $this->getInputName($model, $attribute);
+ if (!array_key_exists('id', $options)) {
+ $options['id'] = $this->getInputId($model, $attribute);
+ }
return Html::input($type, $name, $value, $options);
}
@@ -162,6 +175,9 @@ class ActiveForm extends Widget
{
$value = $this->getAttributeValue($model, $attribute);
$name = $this->getInputName($model, $attribute);
+ if (!array_key_exists('id', $options)) {
+ $options['id'] = $this->getInputId($model, $attribute);
+ }
return Html::textarea($name, $value, $options);
}
@@ -172,6 +188,9 @@ class ActiveForm extends Widget
if (!array_key_exists('uncheck', $options)) {
$options['unchecked'] = '0';
}
+ if (!array_key_exists('id', $options)) {
+ $options['id'] = $this->getInputId($model, $attribute);
+ }
return Html::radio($name, $checked, $value, $options);
}
@@ -182,6 +201,9 @@ class ActiveForm extends Widget
if (!array_key_exists('uncheck', $options)) {
$options['unchecked'] = '0';
}
+ if (!array_key_exists('id', $options)) {
+ $options['id'] = $this->getInputId($model, $attribute);
+ }
return Html::checkbox($name, $checked, $value, $options);
}
@@ -189,6 +211,9 @@ class ActiveForm extends Widget
{
$checked = $this->getAttributeValue($model, $attribute);
$name = $this->getInputName($model, $attribute);
+ if (!array_key_exists('id', $options)) {
+ $options['id'] = $this->getInputId($model, $attribute);
+ }
return Html::dropDownList($name, $checked, $items, $options);
}
@@ -199,6 +224,9 @@ class ActiveForm extends Widget
if (!array_key_exists('unselect', $options)) {
$options['unselect'] = '0';
}
+ if (!array_key_exists('id', $options)) {
+ $options['id'] = $this->getInputId($model, $attribute);
+ }
return Html::listBox($name, $checked, $items, $options);
}
@@ -222,29 +250,6 @@ class ActiveForm extends Widget
return Html::radioList($name, $checked, $items, $options);
}
- public function getInputName($model, $attribute)
- {
- $class = get_class($model);
- if (isset($this->modelMap[$class])) {
- $class = $this->modelMap[$class];
- } elseif (($pos = strrpos($class, '\\')) !== false) {
- $class = substr($class, $pos);
- }
- if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) {
- throw new InvalidParamException('Attribute name must contain word characters only.');
- }
- $prefix = $matches[1];
- $attribute = $matches[2];
- $suffix = $matches[3];
- if ($class === '' && $prefix === '') {
- return $attribute . $suffix;
- } elseif ($class !== '') {
- return $class . $prefix . "[$attribute]" . $suffix;
- } else {
- throw new InvalidParamException('Model name cannot be mapped to empty for tabular inputs.');
- }
- }
-
public function getAttributeValue($model, $attribute)
{
if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) {
@@ -267,7 +272,7 @@ class ActiveForm extends Widget
}
}
- public function normalizeAttributeName($attribute)
+ public function getAttributeName($attribute)
{
if (preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) {
return $matches[2];
@@ -275,4 +280,34 @@ class ActiveForm extends Widget
throw new InvalidParamException('Attribute name must contain word characters only.');
}
}
+
+ /**
+ * @param Model $model
+ * @param string $attribute
+ * @return string
+ * @throws \yii\base\InvalidParamException
+ */
+ public static function getInputName($model, $attribute)
+ {
+ $formName = $model->formName();
+ if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) {
+ throw new InvalidParamException('Attribute name must contain word characters only.');
+ }
+ $prefix = $matches[1];
+ $attribute = $matches[2];
+ $suffix = $matches[3];
+ if ($formName === '' && $prefix === '') {
+ return $attribute . $suffix;
+ } elseif ($formName !== '') {
+ return $formName . $prefix . "[$attribute]" . $suffix;
+ } else {
+ throw new InvalidParamException(get_class($model) . '::formName() cannot be empty for tabular inputs.');
+ }
+ }
+
+ public static function getInputId($model, $attribute)
+ {
+ $name = static::getInputName($model, $attribute);
+ return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name);
+ }
}
diff --git a/framework/widgets/Block.php b/framework/widgets/Block.php
new file mode 100644
index 0000000..d6f7317
--- /dev/null
+++ b/framework/widgets/Block.php
@@ -0,0 +1,49 @@
+
+ * @since 2.0
+ */
+class Block extends Widget
+{
+ /**
+ * @var string the ID of this block.
+ */
+ public $id;
+ /**
+ * @var boolean whether to render the block content in place. Defaults to false,
+ * meaning the captured block content will not be displayed.
+ */
+ public $renderInPlace = false;
+
+ /**
+ * Starts recording a block.
+ */
+ public function init()
+ {
+ ob_start();
+ ob_implicit_flush(false);
+ }
+
+ /**
+ * Ends recording a block.
+ * This method stops output buffering and saves the rendering result as a named block in the controller.
+ */
+ public function run()
+ {
+ $block = ob_get_clean();
+ if ($this->renderInPlace) {
+ echo $block;
+ }
+ $this->view->blocks[$this->id] = $block;
+ }
+}
\ No newline at end of file
diff --git a/framework/widgets/Clip.php b/framework/widgets/Clip.php
deleted file mode 100644
index d540b24..0000000
--- a/framework/widgets/Clip.php
+++ /dev/null
@@ -1,57 +0,0 @@
-
- * @since 2.0
- */
-class Clip extends Widget
-{
- /**
- * @var string the ID of this clip.
- */
- public $id;
- /**
- * @var View the view object for keeping the clip. If not set, the view registered with the application
- * will be used.
- */
- public $view;
- /**
- * @var boolean whether to render the clip content in place. Defaults to false,
- * meaning the captured clip will not be displayed.
- */
- public $renderInPlace = false;
-
- /**
- * Starts recording a clip.
- */
- public function init()
- {
- ob_start();
- ob_implicit_flush(false);
- }
-
- /**
- * Ends recording a clip.
- * This method stops output buffering and saves the rendering result as a named clip in the controller.
- */
- public function run()
- {
- $clip = ob_get_clean();
- if ($this->renderClip) {
- echo $clip;
- }
- $view = $this->view !== null ? $this->view : Yii::$app->getView();
- $view->clips[$this->id] = $clip;
- }
-}
\ No newline at end of file
diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php
index 4c3ae70..3f63621 100644
--- a/framework/widgets/ContentDecorator.php
+++ b/framework/widgets/ContentDecorator.php
@@ -7,10 +7,8 @@
namespace yii\widgets;
-use Yii;
use yii\base\InvalidConfigException;
use yii\base\Widget;
-use yii\base\View;
/**
* @author Qiang Xue
@@ -19,15 +17,10 @@ use yii\base\View;
class ContentDecorator extends Widget
{
/**
- * @var View the view object for rendering [[viewName]]. If not set, the view registered with the application
- * will be used.
+ * @var string 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.
*/
- public $view;
- /**
- * @var string the name of the view that will be used to decorate the content enclosed by this widget.
- * Please refer to [[View::findViewFile()]] on how to set this property.
- */
- public $viewName;
+ public $viewFile;
/**
* @var array the parameters (name=>value) to be extracted and made available in the decorative view.
*/
@@ -38,8 +31,8 @@ class ContentDecorator extends Widget
*/
public function init()
{
- if ($this->viewName === null) {
- throw new InvalidConfigException('ContentDecorator::viewName must be set.');
+ if ($this->viewFile === null) {
+ throw new InvalidConfigException('ContentDecorator::viewFile must be set.');
}
ob_start();
ob_implicit_flush(false);
@@ -53,7 +46,7 @@ class ContentDecorator extends Widget
{
$params = $this->params;
$params['content'] = ob_get_clean();
- $view = $this->view !== null ? $this->view : Yii::$app->getView();
- echo $view->render($this->viewName, $params);
+ // render under the existing context
+ echo $this->view->renderFile($this->viewFile, $params);
}
}
diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php
index 65bb86b..637d115 100644
--- a/framework/widgets/FragmentCache.php
+++ b/framework/widgets/FragmentCache.php
@@ -8,7 +8,6 @@
namespace yii\widgets;
use Yii;
-use yii\base\InvalidConfigException;
use yii\base\Widget;
use yii\caching\Cache;
use yii\caching\Dependency;
@@ -64,11 +63,6 @@ class FragmentCache extends Widget
*/
public $enabled = true;
/**
- * @var \yii\base\View the view object within which this widget is used. If not set,
- * the view registered with the application will be used. This is mainly used by dynamic content feature.
- */
- public $view;
- /**
* @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.
*/
@@ -81,10 +75,6 @@ class FragmentCache extends Widget
{
parent::init();
- if ($this->view === null) {
- $this->view = Yii::$app->getView();
- }
-
if (!$this->enabled) {
$this->cache = null;
} elseif (is_string($this->cache)) {
diff --git a/framework/yiic.php b/framework/yiic.php
index 0db69bb..3872e2f 100644
--- a/framework/yiic.php
+++ b/framework/yiic.php
@@ -14,10 +14,9 @@ defined('STDIN') or define('STDIN', fopen('php://stdin', 'r'));
require(__DIR__ . '/yii.php');
-$id = 'yiic';
-$basePath = __DIR__ . '/console';
-
-$application = new yii\console\Application($id, $basePath, array(
+$application = new yii\console\Application(array(
+ 'id' => 'yiic',
+ 'basePath' => __DIR__ . '/console',
'controllerPath' => '@yii/console/controllers',
));
$application->run();
diff --git a/tests/unit/MysqlTestCase.php b/tests/unit/MysqlTestCase.php
index d62f95e..e1a1f7e 100644
--- a/tests/unit/MysqlTestCase.php
+++ b/tests/unit/MysqlTestCase.php
@@ -4,7 +4,7 @@ namespace yiiunit;
class MysqlTestCase extends TestCase
{
- function __construct()
+ protected function setUp()
{
if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) {
$this->markTestSkipped('pdo and pdo_mysql extensions are required.');
@@ -15,7 +15,7 @@ class MysqlTestCase extends TestCase
* @param bool $reset whether to clean up the test database
* @return \yii\db\Connection
*/
- function getConnection($reset = true)
+ public function getConnection($reset = true)
{
$params = $this->getParam('mysql');
$db = new \yii\db\Connection;
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
index 4a388c6..8290bbe 100644
--- a/tests/unit/bootstrap.php
+++ b/tests/unit/bootstrap.php
@@ -9,4 +9,6 @@ require_once(__DIR__ . '/../../framework/yii.php');
Yii::setAlias('@yiiunit', __DIR__);
+new \yii\web\Application(array('id' => 'testapp', 'basePath' => __DIR__));
+
require_once(__DIR__ . '/TestCase.php');
diff --git a/tests/unit/framework/YiiBaseTest.php b/tests/unit/framework/YiiBaseTest.php
index df12bf9..47474f2 100644
--- a/tests/unit/framework/YiiBaseTest.php
+++ b/tests/unit/framework/YiiBaseTest.php
@@ -1,6 +1,7 @@
aliases = Yii::$aliases;
+ }
+
+ public function tearDown()
+ {
+ Yii::$aliases = $this->aliases;
+ }
+
public function testAlias()
{
+ $this->assertEquals(YII_PATH, Yii::getAlias('@yii'));
+
+ Yii::$aliases = array();
+ $this->assertFalse(Yii::getAlias('@yii', false));
+
+ Yii::setAlias('@yii', '/yii/framework');
+ $this->assertEquals('/yii/framework', Yii::getAlias('@yii'));
+ $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file'));
+ Yii::setAlias('@yii/gii', '/yii/gii');
+ $this->assertEquals('/yii/framework', Yii::getAlias('@yii'));
+ $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file'));
+ $this->assertEquals('/yii/gii', Yii::getAlias('@yii/gii'));
+ $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file'));
+
+ Yii::setAlias('@tii', '@yii/test');
+ $this->assertEquals('/yii/framework/test', Yii::getAlias('@tii'));
+ Yii::setAlias('@yii', null);
+ $this->assertFalse(Yii::getAlias('@yii', false));
+ $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file'));
}
public function testGetVersion()
{
- echo \Yii::getVersion();
+ echo Yii::getVersion();
$this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion()));
}
public function testPowered()
{
- $this->assertTrue(is_string(\Yii::powered()));
+ $this->assertTrue(is_string(Yii::powered()));
}
}
diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php
index 97b0116..74b6e9a 100644
--- a/tests/unit/framework/base/ComponentTest.php
+++ b/tests/unit/framework/base/ComponentTest.php
@@ -41,12 +41,12 @@ class ComponentTest extends TestCase
$component->attachBehavior('a', $behavior);
$this->assertSame($behavior, $component->getBehavior('a'));
$component->on('test', 'fake');
- $this->assertEquals(1, $component->getEventHandlers('test')->count);
+ $this->assertTrue($component->hasEventHandlers('test'));
$clone = clone $component;
$this->assertNotSame($component, $clone);
$this->assertNull($clone->getBehavior('a'));
- $this->assertEquals(0, $clone->getEventHandlers('test')->count);
+ $this->assertFalse($clone->hasEventHandlers('test'));
}
public function testHasProperty()
@@ -151,34 +151,32 @@ class ComponentTest extends TestCase
public function testOn()
{
- $this->assertEquals(0, $this->component->getEventHandlers('click')->getCount());
+ $this->assertFalse($this->component->hasEventHandlers('click'));
$this->component->on('click', 'foo');
- $this->assertEquals(1, $this->component->getEventHandlers('click')->getCount());
- $this->component->on('click', 'bar');
- $this->assertEquals(2, $this->component->getEventHandlers('click')->getCount());
- $p = 'on click';
- $this->component->$p = 'foo2';
- $this->assertEquals(3, $this->component->getEventHandlers('click')->getCount());
+ $this->assertTrue($this->component->hasEventHandlers('click'));
- $this->component->getEventHandlers('click')->add('test');
- $this->assertEquals(4, $this->component->getEventHandlers('click')->getCount());
+ $this->assertFalse($this->component->hasEventHandlers('click2'));
+ $p = 'on click2';
+ $this->component->$p = 'foo2';
+ $this->assertTrue($this->component->hasEventHandlers('click2'));
}
public function testOff()
{
+ $this->assertFalse($this->component->hasEventHandlers('click'));
$this->component->on('click', 'foo');
- $this->component->on('click', array($this->component, 'myEventHandler'));
- $this->assertEquals(2, $this->component->getEventHandlers('click')->getCount());
-
- $result = $this->component->off('click', 'foo');
- $this->assertTrue($result);
- $this->assertEquals(1, $this->component->getEventHandlers('click')->getCount());
- $result = $this->component->off('click', 'foo');
- $this->assertFalse($result);
- $this->assertEquals(1, $this->component->getEventHandlers('click')->getCount());
- $result = $this->component->off('click', array($this->component, 'myEventHandler'));
- $this->assertTrue($result);
- $this->assertEquals(0, $this->component->getEventHandlers('click')->getCount());
+ $this->assertTrue($this->component->hasEventHandlers('click'));
+ $this->component->off('click', 'foo');
+ $this->assertFalse($this->component->hasEventHandlers('click'));
+
+ $this->component->on('click2', 'foo');
+ $this->component->on('click2', 'foo2');
+ $this->component->on('click2', 'foo3');
+ $this->assertTrue($this->component->hasEventHandlers('click2'));
+ $this->component->off('click2', 'foo3');
+ $this->assertTrue($this->component->hasEventHandlers('click2'));
+ $this->component->off('click2');
+ $this->assertFalse($this->component->hasEventHandlers('click2'));
}
public function testTrigger()
diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php
index aa15230..f04e550 100644
--- a/tests/unit/framework/base/ModelTest.php
+++ b/tests/unit/framework/base/ModelTest.php
@@ -195,7 +195,7 @@ class ModelTest extends TestCase
public function testCreateValidators()
{
- $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must be an array specifying both attribute names and validator type.');
+ $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must specify both attribute names and validator type.');
$invalid = new InvalidRulesModel();
$invalid->createValidators();
diff --git a/tests/unit/framework/caching/CacheTest.php b/tests/unit/framework/caching/CacheTest.php
index ad2fcf5..f9db4f4 100644
--- a/tests/unit/framework/caching/CacheTest.php
+++ b/tests/unit/framework/caching/CacheTest.php
@@ -16,9 +16,9 @@ abstract class CacheTest extends TestCase
public function testSet()
{
$cache = $this->getCacheInstance();
- $cache->set('string_test', 'string_test');
- $cache->set('number_test', 42);
- $cache->set('array_test', array('array_test' => 'array_test'));
+ $this->assertTrue($cache->set('string_test', 'string_test'));
+ $this->assertTrue($cache->set('number_test', 42));
+ $this->assertTrue($cache->set('array_test', array('array_test' => 'array_test')));
$cache['arrayaccess_test'] = new \stdClass();
}
@@ -45,7 +45,7 @@ abstract class CacheTest extends TestCase
public function testExpire()
{
$cache = $this->getCacheInstance();
- $cache->set('expire_test', 'expire_test', 2);
+ $this->assertTrue($cache->set('expire_test', 'expire_test', 2));
sleep(1);
$this->assertEquals('expire_test', $cache->get('expire_test'));
sleep(2);
@@ -57,11 +57,11 @@ abstract class CacheTest extends TestCase
$cache = $this->getCacheInstance();
// should not change existing keys
- $cache->add('number_test', 13);
+ $this->assertFalse($cache->add('number_test', 13));
$this->assertEquals(42, $cache->get('number_test'));
// should store data is it's not there yet
- $cache->add('add_test', 13);
+ $this->assertTrue($cache->add('add_test', 13));
$this->assertEquals(13, $cache->get('add_test'));
}
@@ -69,14 +69,14 @@ abstract class CacheTest extends TestCase
{
$cache = $this->getCacheInstance();
- $cache->delete('number_test');
+ $this->assertTrue($cache->delete('number_test'));
$this->assertEquals(null, $cache->get('number_test'));
}
public function testFlush()
{
$cache = $this->getCacheInstance();
- $cache->flush();
+ $this->assertTrue($cache->flush());
$this->assertEquals(null, $cache->get('add_test'));
}
}
diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php
index 3977ee8..594e946 100644
--- a/tests/unit/framework/caching/DbCacheTest.php
+++ b/tests/unit/framework/caching/DbCacheTest.php
@@ -11,7 +11,7 @@ class DbCacheTest extends CacheTest
private $_cacheInstance;
private $_connection;
- function __construct()
+ protected function setUp()
{
if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) {
$this->markTestSkipped('pdo and pdo_mysql extensions are required.');
diff --git a/tests/unit/framework/db/ConnectionTest.php b/tests/unit/framework/db/ConnectionTest.php
index afb4f20..256c5a9 100644
--- a/tests/unit/framework/db/ConnectionTest.php
+++ b/tests/unit/framework/db/ConnectionTest.php
@@ -59,7 +59,6 @@ class ConnectionTest extends \yiiunit\MysqlTestCase
$this->assertEquals('`table`', $connection->quoteTableName('`table`'));
$this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.table'));
$this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.`table`'));
- $this->assertEquals('[[table]]', $connection->quoteTableName('[[table]]'));
$this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}'));
$this->assertEquals('(table)', $connection->quoteTableName('(table)'));
}
diff --git a/tests/unit/framework/util/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php
similarity index 97%
rename from tests/unit/framework/util/ArrayHelperTest.php
rename to tests/unit/framework/helpers/ArrayHelperTest.php
index 117c702..187217f 100644
--- a/tests/unit/framework/util/ArrayHelperTest.php
+++ b/tests/unit/framework/helpers/ArrayHelperTest.php
@@ -1,6 +1,6 @@
'test',
+ 'basePath' => '@yiiunit/runtime',
'components' => array(
'request' => array(
'class' => 'yii\web\Request',
@@ -20,6 +22,14 @@ class HtmlTest extends \yii\test\TestCase
));
}
+ public function assertEqualsWithoutLE($expected, $actual)
+ {
+ $expected = str_replace("\r\n", "\n", $expected);
+ $actual = str_replace("\r\n", "\n", $actual);
+
+ $this->assertEquals($expected, $actual);
+ }
+
public function tearDown()
{
Yii::$app = null;
@@ -238,21 +248,21 @@ class HtmlTest extends \yii\test\TestCase
EOD;
- $this->assertEquals($expected, Html::dropDownList('test'));
+ $this->assertEqualsWithoutLE($expected, Html::dropDownList('test'));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems()));
+ $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', null, $this->getDataItems()));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems()));
+ $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', 'value2', $this->getDataItems()));
}
public function testListBox()
@@ -262,48 +272,48 @@ EOD;
EOD;
- $this->assertEquals($expected, Html::listBox('test'));
+ $this->assertEqualsWithoutLE($expected, Html::listBox('test'));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5)));
+ $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5)));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2()));
+ $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems2()));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems()));
+ $this->assertEqualsWithoutLE($expected, Html::listBox('test', 'value2', $this->getDataItems()));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems()));
+ $this->assertEqualsWithoutLE($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems()));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true)));
+ $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, array(), array('multiple' => true)));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0')));
+ $this->assertEqualsWithoutLE($expected, Html::listBox('test', '', array(), array('unselect' => '0')));
}
public function testCheckboxList()
@@ -314,19 +324,19 @@ EOD;
EOD;
- $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems()));
+ $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems()));
$expected = << text1<>
EOD;
- $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2()));
+ $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2()));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array(
+ $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array(
'separator' => " \n",
'unselect' => '0',
)));
@@ -335,7 +345,7 @@ EOD;
0
1
EOD;
- $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array(
+ $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array(
'item' => function ($index, $label, $name, $checked, $value) {
return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value));
}
@@ -350,19 +360,19 @@ EOD;
EOD;
- $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems()));
+ $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems()));
$expected = << text1<>
EOD;
- $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2()));
+ $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems2()));
$expected = <<
EOD;
- $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array(
+ $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array(
'separator' => " \n",
'unselect' => '0',
)));
@@ -371,7 +381,7 @@ EOD;
0
1
EOD;
- $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array(
+ $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array(
'item' => function ($index, $label, $name, $checked, $value) {
return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value));
}
@@ -418,7 +428,7 @@ EOD;
'group12' => array('class' => 'group'),
),
);
- $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes));
+ $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes));
}
public function testRenderAttributes()
diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php
new file mode 100644
index 0000000..4e1266f
--- /dev/null
+++ b/tests/unit/framework/helpers/StringHelperTest.php
@@ -0,0 +1,73 @@
+assertEquals(4, StringHelper::strlen('this'));
+ $this->assertEquals(6, StringHelper::strlen('это'));
+ }
+
+ public function testSubstr()
+ {
+ $this->assertEquals('th', StringHelper::substr('this', 0, 2));
+ $this->assertEquals('э', StringHelper::substr('это', 0, 2));
+ }
+
+ public function testPluralize()
+ {
+ $testData = array(
+ 'move' => 'moves',
+ 'foot' => 'feet',
+ 'child' => 'children',
+ 'human' => 'humans',
+ 'man' => 'men',
+ 'staff' => 'staff',
+ 'tooth' => 'teeth',
+ 'person' => 'people',
+ 'mouse' => 'mice',
+ 'touch' => 'touches',
+ 'hash' => 'hashes',
+ 'shelf' => 'shelves',
+ 'potato' => 'potatoes',
+ 'bus' => 'buses',
+ 'test' => 'tests',
+ 'car' => 'cars',
+ );
+
+ foreach($testData as $testIn => $testOut) {
+ $this->assertEquals($testOut, StringHelper::pluralize($testIn));
+ $this->assertEquals(ucfirst($testOut), ucfirst(StringHelper::pluralize($testIn)));
+ }
+ }
+
+ public function testCamel2words()
+ {
+ $this->assertEquals('Camel Case', StringHelper::camel2words('camelCase'));
+ $this->assertEquals('Lower Case', StringHelper::camel2words('lower_case'));
+ $this->assertEquals('Tricky Stuff It Is Testing', StringHelper::camel2words(' tricky_stuff.it-is testing... '));
+ }
+
+ public function testCamel2id()
+ {
+ $this->assertEquals('post-tag', StringHelper::camel2id('PostTag'));
+ $this->assertEquals('post_tag', StringHelper::camel2id('PostTag', '_'));
+
+ $this->assertEquals('post-tag', StringHelper::camel2id('postTag'));
+ $this->assertEquals('post_tag', StringHelper::camel2id('postTag', '_'));
+ }
+
+ public function testId2camel()
+ {
+ $this->assertEquals('PostTag', StringHelper::id2camel('post-tag'));
+ $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_'));
+
+ $this->assertEquals('PostTag', StringHelper::id2camel('post-tag'));
+ $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_'));
+ }
+}
\ No newline at end of file