diff --git a/docs/api/base/Component.md b/docs/api/base/Component.md index 89b8c0b..01ef462 100644 --- a/docs/api/base/Component.md +++ b/docs/api/base/Component.md @@ -1,3 +1,5 @@ +Component is the base class that implements the *property*, *event* and *behavior* features. + Component provides the *event* and *behavior* features, in addition to the *property* feature which is implemented in its parent class [[Object]]. @@ -7,20 +9,20 @@ is triggered (i.e. comment will be added), our custom code will be executed. An event is identified by a name that should be unique within the class it is defined at. Event names are *case-sensitive*. -One or multiple PHP callbacks, called *event handlers*, could be attached to an event. You can call [[trigger()]] to +One or multiple PHP callbacks, called *event handlers*, can be attached to an event. You can call [[trigger()]] to raise an event. When an event is raised, the event handlers will be invoked automatically in the order they were attached. To attach an event handler to an event, call [[on()]]: ~~~ -$comment->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 ` '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 e81a288..6df998b 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -46,8 +46,9 @@ 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 */ @@ -113,7 +114,7 @@ class YiiBase * 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 + * 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. @@ -188,10 +189,10 @@ class YiiBase * it will be returned back without change. * * Note, this method does not ensure the existence of the resulting path. - * @param string $alias alias + * @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 */ @@ -225,13 +226,14 @@ class YiiBase * 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. * - * @param string $alias alias to the path. The alias must start with '@'. + * @param string $alias the alias name (e.g. "@yii"). It should start with a '@' character + * and should NOT contain the forward slash "/" or the backward slash "\". * @param string $path the path corresponding to the alias. 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]]. + * actual path first by calling [[getAlias()]]. * * @throws Exception if $path is an invalid alias * @see getAlias @@ -268,48 +270,35 @@ class YiiBase */ 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'; - } - } 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'; + if (isset(self::$classMap[$className])) { + $classFile = self::$classMap[$className]; + } else { + if (($pos = strrpos($className, '\\')) !== false) { + // namespaced class, e.g. yii\base\Component + $classFile = str_replace('\\', '/', substr($className, 0, $pos + 1)) + . str_replace('_', '/', substr($className, $pos + 1)) . '.php'; + } else { + $classFile = 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; - } + if (strpos($classFile, '/') !== false) { + // make it into a path alias + $classFile = '@' . $classFile; } } - if (isset($classFile, $alias) && is_file($classFile)) { - if (!YII_DEBUG || basename(realpath($classFile)) === basename($alias) . '.php') { - include($classFile); + $classFile = static::getAlias($classFile); + if ($classFile !== false && is_file($classFile)) { + include($classFile); + if (class_exists($className, false) || interface_exists($className, false)) { return true; } else { - throw new Exception("Class name '$className' does not match the class file '" . realpath($classFile) . "'. Have you checked their case sensitivity?"); + throw new Exception("Unable to find '$className' in file: $classFile"); } + } else { + return false; } - - return false; } /** @@ -371,6 +360,12 @@ class YiiBase $class = static::import($class, true); } + $class = ltrim($class, '\\'); + + if (isset(self::$objectConfig[$class])) { + $config = array_merge(self::$objectConfig[$class], $config); + } + if (($n = func_num_args()) > 1) { /** @var $reflection \ReflectionClass */ if (isset($reflections[$class])) { @@ -531,6 +526,6 @@ class YiiBase */ 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/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..e1c1d60 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -87,9 +87,6 @@ class Application extends Module */ public $layout = 'main'; - // todo - public $localeDataPath = '@yii/i18n/data'; - private $_runtimePath; private $_ended = false; @@ -101,36 +98,42 @@ class Application extends Module /** * 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']); + unset($config['basePath']); + Yii::$aliases['@app'] = $this->getBasePath(); + } 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()); + } } /** @@ -165,31 +168,6 @@ class Application extends Module 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) { @@ -295,37 +273,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 +337,6 @@ class Application extends Module } /** - * Sets default path aliases. - */ - public function registerDefaultAliases() - { - Yii::$aliases['@app'] = $this->getBasePath(); - } - - /** * Registers the core application components. * @see setComponents */ 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 241c66c..6ff68da 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -357,7 +357,8 @@ class Controller extends Component */ public function renderPartial($view, $params = array()) { - return $this->getView()->render($view, $params, $this); + $viewFile = $this->findViewFile($view); + return $this->getView()->renderFile($viewFile, $params, $this); } /** diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php index 465d839..93390e7 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); + } } /** @@ -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/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/Model.php b/framework/base/Model.php index 611b12e..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. diff --git a/framework/base/Module.php b/framework/base/Module.php index 3e7eb04..2ccf61d 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(); } @@ -580,8 +579,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,21 +605,14 @@ 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')) { + $className = ltrim($this->controllerNamespace . '\\' . $className, '\\'); + Yii::$classMap[$className] = $classFile; + if (class_exists($className)) { + if (is_subclass_of($className, 'yii\base\Controller')) { $controller = new $className($id, $this); - } elseif (YII_DEBUG) { - if (!class_exists($className, false)) { - throw new InvalidConfigException("Class file name does not match class name: $className."); - } elseif (!is_subclass_of($className, '\yii\base\Controller')) { - throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); - } + } elseif (YII_DEBUG && !is_subclass_of($className, 'yii\base\Controller')) { + 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/Theme.php b/framework/base/Theme.php index 88ecb0a..e529a63 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. @@ -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("Theme::baseUrl 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/View.php b/framework/base/View.php index d3d9339..8b1f4ef 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -22,10 +22,23 @@ use yii\helpers\FileHelper; class View extends Component { /** + * @event Event an event that is triggered by [[renderFile()]] right before it renders a view file. + */ + const EVENT_BEFORE_RENDER = 'beforeRender'; + /** + * @event Event an event that is triggered by [[renderFile()]] right after it renders a view file. + */ + const EVENT_AFTER_RENDER = 'afterRender'; + + /** * @var object the object that owns this view. This can be a controller, a widget, or any other object. */ public $context; /** + * @var ViewContent + */ + public $content; + /** * @var mixed custom parameters that are shared among view templates. */ public $params; @@ -47,7 +60,7 @@ class View extends Component public $clips; /** * @var Widget[] the widgets that are currently being rendered (not ended). This property - * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. + * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it. */ public $widgetStack = array(); /** @@ -74,6 +87,11 @@ class View extends Component if (is_array($this->theme)) { $this->theme = Yii::createObject($this->theme); } + if (is_array($this->content)) { + $this->content = Yii::createObject($this->content); + } else { + $this->content = new ViewContent; + } } /** @@ -140,10 +158,15 @@ 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); + } + $output = $this->content->populate($output); + $this->afterRender($viewFile, $output); } $this->context = $oldContext; @@ -152,6 +175,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. diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php new file mode 100644 index 0000000..6a3b489 --- /dev/null +++ b/framework/base/ViewContent.php @@ -0,0 +1,136 @@ + + * @since 2.0 + */ +class ViewContent extends Component +{ + const POS_HEAD = 1; + const POS_BEGIN = 2; + const POS_END = 3; + + /** + * @var array + * + * Each asset bundle should be declared with the following structure: + * + * ~~~ + * array( + * 'basePath' => '...', + * 'baseUrl' => '...', // if missing, the bundle will be published to the "www/assets" folder + * 'js' => array( + * 'js/main.js', + * 'js/menu.js', + * 'js/base.js' => self::POS_HEAD, + * 'css' => array( + * 'css/main.css', + * 'css/menu.css', + * ), + * 'depends' => array( + * 'jquery', + * 'yii', + * 'yii/treeview', + * ), + * ) + * ~~~ + */ + public $bundles; + + public $title; + public $metaTags; + public $linkTags; + public $css; + public $js; + public $cssFiles; + public $jsFiles; + + public function populate($content) + { + return $content; + } + + public function reset() + { + $this->title = null; + $this->metaTags = null; + $this->linkTags = null; + $this->css = null; + $this->js = null; + $this->cssFiles = null; + $this->jsFiles = null; + } + + public function registerBundle($name) + { + + } + + public function getMetaTag($key) + { + return isset($this->metaTags[$key]) ? $this->metaTags[$key] : null; + } + + public function setMetaTag($key, $tag) + { + $this->metaTags[$key] = $tag; + } + + public function getLinkTag($key) + { + return isset($this->linkTags[$key]) ? $this->linkTags[$key] : null; + } + + public function setLinkTag($key, $tag) + { + $this->linkTags[$key] = $tag; + } + + public function getCss($key) + { + return isset($this->css[$key]) ? $this->css[$key]: null; + } + + public function setCss($key, $css) + { + $this->css[$key] = $css; + } + + public function getCssFile($key) + { + return isset($this->cssFiles[$key]) ? $this->cssFiles[$key]: null; + } + + public function setCssFile($key, $file) + { + $this->cssFiles[$key] = $file; + } + + public function getJs($key, $position = self::POS_END) + { + return isset($this->js[$position][$key]) ? $this->js[$position][$key] : null; + } + + public function setJs($key, $js, $position = self::POS_END) + { + $this->js[$position][$key] = $js; + } + + public function getJsFile($key, $position = self::POS_END) + { + return isset($this->jsFiles[$position][$key]) ? $this->jsFiles[$position][$key] : null; + } + + public function setJsFile($key, $file, $position = self::POS_END) + { + $this->jsFiles[$position][$key] = $file; + } + +} \ 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/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php index af34e9d..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,9 +52,6 @@ class ChainedDependency extends Dependency public function evaluateDependency() { foreach ($this->dependencies as $dependency) { - if (!$dependency instanceof Dependency) { - $dependency = \Yii::createObject($dependency); - } $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/DbDependency.php b/framework/caching/DbDependency.php index 4308dc1..cbe0ae1 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -28,23 +28,25 @@ class DbDependency extends Dependency public $db = 'db'; /** * @var string the SQL query whose result is used to determine if the dependency has been changed. - * Only the first row of the query result will be used. This property must be always set, otherwise - * an exception would be raised. + * Only the first row of the query result will be used. */ public $sql; /** * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. */ - public $params = array(); + public $params; /** - * Initializes the database dependency object. + * Constructor. + * @param string $sql the SQL query whose result is used to determine if the dependency has been changed. + * @param array $params the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. + * @param array $config name-value pairs that will be used to initialize the object properties */ - public function init() + public function __construct($sql, $params = array(), $config = array()) { - if ($this->sql === null) { - throw new InvalidConfigException('DbDependency::sql must be set.'); - } + $this->sql = $sql; + $this->params = $params; + parent::__construct($config); } /** diff --git a/framework/caching/ExpressionDependency.php b/framework/caching/ExpressionDependency.php index bf70291..e13c962 100644 --- a/framework/caching/ExpressionDependency.php +++ b/framework/caching/ExpressionDependency.php @@ -22,7 +22,18 @@ class ExpressionDependency extends Dependency /** * @var string the PHP expression whose result is used to determine the dependency. */ - public $expression = 'true'; + public $expression; + + /** + * Constructor. + * @param string $expression the PHP expression whose result is used to determine the dependency. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($expression = 'true', $config = array()) + { + $this->expression = $expression; + parent::__construct($config); + } /** * Generates the data needed to determine if dependency has been changed. diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 8d858ec..3797dde 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -7,8 +7,6 @@ namespace yii\caching; -use yii\base\InvalidConfigException; - /** * FileDependency represents a dependency based on a file's last modification time. * @@ -22,19 +20,19 @@ class FileDependency extends Dependency { /** * @var string the name of the file whose last modification time is used to - * check if the dependency has been changed. This property must be always set, - * otherwise an exception would be raised. + * check if the dependency has been changed. */ public $fileName; /** - * Initializes the database dependency object. + * Constructor. + * @param string $fileName name of the file whose change is to be checked. + * @param array $config name-value pairs that will be used to initialize the object properties */ - public function init() + public function __construct($fileName = null, $config = array()) { - if ($this->file === null) { - throw new InvalidConfigException('FileDependency::fileName must be set.'); - } + $this->fileName = $fileName; + parent::__construct($config); } /** diff --git a/framework/logging/FileTarget.php b/framework/logging/FileTarget.php index c3f4031..69799cd 100644 --- a/framework/logging/FileTarget.php +++ b/framework/logging/FileTarget.php @@ -6,6 +6,8 @@ */ namespace yii\logging; + +use Yii; use yii\base\InvalidConfigException; /** @@ -23,15 +25,14 @@ use yii\base\InvalidConfigException; class FileTarget extends Target { /** - * @var string log file path or path alias. If not set, it means the 'application.log' file under - * the application runtime directory. Please make sure the directory containing - * the log file is writable by the Web server process. + * @var string log file path or path alias. If not set, it will use the "runtime/logs/app.log" file. + * The directory containing the log files will be automatically created if not existing. */ public $logFile; /** - * @var integer maximum log file size, in kilo-bytes. Defaults to 1024, meaning 1MB. + * @var integer maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB. */ - public $maxFileSize = 1024; // in KB + public $maxFileSize = 10240; // in KB /** * @var integer number of log files used for rotation. Defaults to 5. */ @@ -46,13 +47,13 @@ class FileTarget extends Target { parent::init(); if ($this->logFile === null) { - $this->logFile = \Yii::$app->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; + $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log'; } else { - $this->logFile = \Yii::getAlias($this->logFile); + $this->logFile = Yii::getAlias($this->logFile); } $logPath = dirname($this->logFile); - if (!is_dir($logPath) || !is_writable($logPath)) { - throw new InvalidConfigException("Directory '$logPath' does not exist or is not writable."); + if (!is_dir($logPath)) { + @mkdir($logPath, 0777, true); } if ($this->maxLogFiles < 1) { $this->maxLogFiles = 1; @@ -66,6 +67,7 @@ class FileTarget extends Target * Sends log messages to specified email addresses. * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure * of each message. + * @throws InvalidConfigException if unable to open the log file for writing */ public function export($messages) { @@ -73,7 +75,9 @@ class FileTarget extends Target foreach ($messages as $message) { $text .= $this->formatMessage($message); } - $fp = @fopen($this->logFile, 'a'); + if (($fp = @fopen($this->logFile, 'a')) === false) { + throw new InvalidConfigException("Unable to append to log file: {$this->logFile}"); + } @flock($fp, LOCK_EX); if (@filesize($this->logFile) > $this->maxFileSize * 1024) { $this->rotateFiles(); diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 427fa44..6d2c671 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * BooleanValidator checks if the attribute value is a boolean value. * @@ -32,11 +34,17 @@ class BooleanValidator extends Validator * Defaults to false, meaning only the value needs to be matched. */ public $strict = false; + /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * Initializes the validator. */ - public $allowEmpty = true; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} must be either "{true}" or "{false}".'); + } + } /** * Validates the attribute of the object. @@ -47,13 +55,8 @@ class BooleanValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if (!$this->strict && $value != $this->trueValue && $value != $this->falseValue - || $this->strict && $value !== $this->trueValue && $value !== $this->falseValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); - $this->addError($object, $attribute, $message, array( + if (!$this->validateValue($value)) { + $this->addError($object, $attribute, $this->message, array( '{true}' => $this->trueValue, '{false}' => $this->falseValue, )); @@ -61,6 +64,17 @@ class BooleanValidator extends Validator } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return $this->strict && ($value == $this->trueValue || $value == $this->falseValue) + || !$this->strict && ($value === $this->trueValue || $value === $this->falseValue); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. @@ -68,15 +82,14 @@ class BooleanValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, '{true}' => $this->trueValue, '{false}' => $this->falseValue, )); return " -if(" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . "value!=" . json_encode($this->trueValue) . " && value!=" . json_encode($this->falseValue) . ") { +if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . "value!=" . json_encode($this->trueValue) . " && value!=" . json_encode($this->falseValue) . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 3f31f77..3b4745b 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -7,6 +7,9 @@ namespace yii\validators; +use Yii; +use yii\base\InvalidConfigException; + /** * CaptchaValidator validates that the attribute value is the same as the verification code displayed in the CAPTCHA. * @@ -22,16 +25,21 @@ class CaptchaValidator extends Validator */ public $caseSensitive = false; /** - * @var string the ID of the action that renders the CAPTCHA image. Defaults to 'captcha', - * meaning the `captcha` action declared in the current controller. - * This can also be a route consisting of controller ID and action ID (e.g. 'site/captcha'). + * @var string the route of the controller action that renders the CAPTCHA image. */ - public $captchaAction = 'captcha'; + public $captchaAction = 'site/captcha'; + + /** - * @var boolean whether the attribute value can be null or empty. - * Defaults to false, meaning the attribute is invalid if it is empty. + * Initializes the validator. */ - public $allowEmpty = false; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|The verification code is incorrect.'); + } + } /** * Validates the attribute of the object. @@ -42,36 +50,38 @@ class CaptchaValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; + if (!$this->validateValue($value)) { + $this->addError($object, $attribute, $this->message); } + } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { $captcha = $this->getCaptchaAction(); - if (!$captcha->validate($value, $this->caseSensitive)) { - $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); - $this->addError($object, $attribute, $message); - } + return !is_array($value) && $captcha->validate($value, $this->caseSensitive); } /** * Returns the CAPTCHA action object. - * @return CCaptchaAction the action object + * @return CaptchaAction the action object */ public function getCaptchaAction() { - if (strpos($this->captchaAction, '/') !== false) { // contains controller or module - $ca = \Yii::$app->createController($this->captchaAction); - if ($ca !== null) { - list($controller, $actionID) = $ca; - $action = $controller->createAction($actionID); + $ca = Yii::$app->createController($this->captchaAction); + if ($ca !== false) { + /** @var \yii\base\Controller $controller */ + list($controller, $actionID) = $ca; + $action = $controller->createAction($actionID); + if ($action !== null) { + return $action; } - } else { - $action = \Yii::$app->getController()->createAction($this->captchaAction); - } - - if ($action === null) { - throw new \yii\base\Exception('Invalid captcha action ID: ' . $this->captchaAction); } - return $action; + throw new InvalidConfigException('Invalid CAPTCHA action ID: ' . $this->captchaAction); } /** @@ -83,8 +93,7 @@ class CaptchaValidator extends Validator public function clientValidateAttribute($object, $attribute) { $captcha = $this->getCaptchaAction(); - $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -102,7 +111,7 @@ if(h != hash) { } "; - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if($.trim(value)!='') { $js diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 43f2edf..1df09c4 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -6,6 +6,7 @@ */ namespace yii\validators; + use Yii; use yii\base\InvalidConfigException; @@ -45,23 +46,12 @@ class CompareValidator extends Validator */ public $compareValue; /** - * @var boolean whether the comparison is strict (both value and type must be the same.) - * Defaults to false. - */ - public $strict = false; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to false. - * If this is true, it means the attribute is considered valid when it is empty. - */ - public $allowEmpty = false; - /** - * @var string the operator for comparison. Defaults to '='. - * The followings are valid operators: - * - * - `=` or `==`: validates to see if the two values are equal. If [[strict]] is true, the comparison - * will be done in strict mode (i.e. checking value type as well). - * - `!=`: validates to see if the two values are NOT equal. If [[strict]] is true, the comparison - * will be done in strict mode (i.e. checking value type as well). + * @var string the operator for comparison. The following operators are supported: + * + * - '==': validates to see if the two values are equal. The comparison is done is non-strict mode. + * - '===': validates to see if the two values are equal. The comparison is done is strict mode. + * - '!=': validates to see if the two values are NOT equal. The comparison is done is non-strict mode. + * - '!==': validates to see if the two values are NOT equal. The comparison is done is strict mode. * - `>`: validates to see if the value being validated is greater than the value being compared with. * - `>=`: validates to see if the value being validated is greater than or equal to the value being compared with. * - `<`: validates to see if the value being validated is less than the value being compared with. @@ -69,6 +59,45 @@ class CompareValidator extends Validator */ public $operator = '='; + + /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + switch ($this->operator) { + case '==': + $this->message = Yii::t('yii|{attribute} must be repeated exactly.'); + break; + case '===': + $this->message = Yii::t('yii|{attribute} must be repeated exactly.'); + break; + case '!=': + $this->message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); + break; + case '!==': + $this->message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); + break; + case '>': + $this->message = Yii::t('yii|{attribute} must be greater than "{compareValue}".'); + break; + case '>=': + $this->message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); + break; + case '<': + $this->message = Yii::t('yii|{attribute} must be less than "{compareValue}".'); + break; + case '<=': + $this->message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); + break; + default: + throw new InvalidConfigException("Unknown operator: {$this->operator}"); + } + } + } + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. @@ -79,7 +108,8 @@ class CompareValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); return; } if ($this->compareValue !== null) { @@ -91,45 +121,45 @@ class CompareValidator extends Validator } switch ($this->operator) { - case '=': - case '==': - if (($this->strict && $value !== $compareValue) || (!$this->strict && $value != $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); - } - break; - case '!=': - if (($this->strict && $value === $compareValue) || (!$this->strict && $value == $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '>': - if ($value <= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '>=': - if ($value < $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '<': - if ($value >= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '<=': - if ($value > $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - default: - throw new InvalidConfigException("Unknown operator: {$this->operator}"); + case '==': $valid = $value == $compareValue; break; + case '===': $valid = $value === $compareValue; break; + case '!=': $valid = $value != $compareValue; break; + case '!==': $valid = $value !== $compareValue; break; + case '>': $valid = $value > $compareValue; break; + case '>=': $valid = $value >= $compareValue; break; + case '<': $valid = $value < $compareValue; break; + case '<=': $valid = $value <= $compareValue; break; + default: $valid = false; break; + } + if (!$valid) { + $this->addError($object, $attribute, $this->message, array( + '{compareAttribute}' => $compareLabel, + '{compareValue}' => $compareValue, + )); + } + } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + * @throws InvalidConfigException if [[compareValue]] is not set. + */ + public function validateValue($value) + { + if ($this->compareValue === null) { + throw new InvalidConfigException('CompareValidator::compareValue must be set.'); + } + + switch ($this->operator) { + case '==': return $value == $this->compareValue; + case '===': return $value === $this->compareValue; + case '!=': return $value != $this->compareValue; + case '!==': return $value !== $this->compareValue; + case '>': return $value > $this->compareValue; + case '>=': return $value >= $this->compareValue; + case '<': return $value < $this->compareValue; + case '<=': return $value <= $this->compareValue; } } @@ -150,57 +180,14 @@ class CompareValidator extends Validator $compareValue = "\$('#" . (CHtml::activeId($object, $compareAttribute)) . "').val()"; $compareLabel = $object->getAttributeLabel($compareAttribute); } - - $message = $this->message; - switch ($this->operator) { - case '=': - case '==': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be repeated exactly.'); - } - $condition = 'value!=' . $compareValue; - break; - case '!=': - if ($message === null) { - $message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); - } - $condition = 'value==' . $compareValue; - break; - case '>': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be greater than "{compareValue}".'); - } - $condition = 'value<=' . $compareValue; - break; - case '>=': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); - } - $condition = 'value<' . $compareValue; - break; - case '<': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be less than "{compareValue}".'); - } - $condition = 'value>=' . $compareValue; - break; - case '<=': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); - } - $condition = 'value>' . $compareValue; - break; - default: - throw new InvalidConfigException("Unknown operator: {$this->operator}"); - } - - $message = strtr($message, array( + $condition = "value {$this->operator} $compareValue"; + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{compareValue}' => $compareLabel, )); return " -if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { +if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index 7899c95..7c3b181 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -7,11 +7,11 @@ namespace yii\validators; +use Yii; +use DateTime; + /** - * DateValidator verifies if the attribute represents a date, time or datetime. - * - * By setting the {@link format} property, one can specify what format the date value - * must be in. If the given date value doesn't follow the format, the attribute is considered as invalid. + * DateValidator verifies if the attribute represents a date, time or datetime in a proper format. * * @author Qiang Xue * @since 2.0 @@ -19,17 +19,11 @@ namespace yii\validators; class DateValidator extends Validator { /** - * @var mixed the format pattern that the date value should follow. - * This can be either a string or an array representing multiple formats. - * Defaults to 'MM/dd/yyyy'. Please see {@link CDateTimeParser} for details - * about how to specify a date format. - */ - public $format = 'MM/dd/yyyy'; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * @var string the date format that the value being validated should follow. + * Please refer to [[http://www.php.net/manual/en/datetime.createfromformat.php]] on + * supported formats. */ - public $allowEmpty = true; + public $format = 'Y-m-d'; /** * @var string the name of the attribute to receive the parsing result. * When this property is not null and the validation is successful, the named attribute will @@ -38,6 +32,17 @@ class DateValidator extends Validator public $timestampAttribute; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|The format of {attribute} is invalid.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -46,27 +51,26 @@ class DateValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + if (is_array($value)) { + $this->addError($object, $attribute, $this->message); return; } - - $formats = is_string($this->format) ? array($this->format) : $this->format; - $valid = false; - foreach ($formats as $format) { - $timestamp = CDateTimeParser::parse($value, $format, array('month' => 1, 'day' => 1, 'hour' => 0, 'minute' => 0, 'second' => 0)); - if ($timestamp !== false) { - $valid = true; - if ($this->timestampAttribute !== null) { - $object-> {$this->timestampAttribute} = $timestamp; - } - break; - } + $date = DateTime::createFromFormat($this->format, $value); + if ($date === false) { + $this->addError($object, $attribute, $this->message); + } elseif ($this->timestampAttribute !== false) { + $object->{$this->timestampAttribute} = $date->getTimestamp(); } + } - if (!$valid) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|The format of {attribute} is invalid.'); - $this->addError($object, $attribute, $message); - } + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return DateTime::createFromFormat($this->format, $value) !== false; } } diff --git a/framework/validators/DefaultValueValidator.php b/framework/validators/DefaultValueValidator.php index be06768..185dbd4 100644 --- a/framework/validators/DefaultValueValidator.php +++ b/framework/validators/DefaultValueValidator.php @@ -10,12 +10,8 @@ namespace yii\validators; /** * DefaultValueValidator sets the attribute to be the specified default value. * - * By default, when the attribute being validated is [[isEmpty|empty]], the validator - * will assign a default [[value]] to it. However, if [[setOnEmpty]] is false, the validator - * will always assign the default [[value]] to the attribute, no matter it is empty or not. - * * DefaultValueValidator is not really a validator. It is provided mainly to allow - * specifying attribute default values in a dynamic way. + * specifying attribute default values when they are empty. * * @author Qiang Xue * @since 2.0 @@ -27,11 +23,10 @@ class DefaultValueValidator extends Validator */ public $value; /** - * @var boolean whether to set the default [[value]] only when the attribute is [[isEmpty|empty]]. - * Defaults to true. If false, the attribute will always be assigned with the default [[value]], - * no matter it is empty or not. + * @var boolean this property is overwritten to be false so that this validator will + * be applied when the value being validated is empty. */ - public $setOnEmpty = true; + public $skipOnEmpty = false; /** * Validates the attribute of the object. @@ -40,7 +35,7 @@ class DefaultValueValidator extends Validator */ public function validateAttribute($object, $attribute) { - if (!$this->setOnEmpty || $this->isEmpty($object->$attribute)) { + if ($this->isEmpty($object->$attribute)) { $object->$attribute = $this->value; } } diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index d1d2257..e498975 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * EmailValidator validates that the attribute value is a valid email address. * @@ -42,11 +44,17 @@ class EmailValidator extends Validator * Defaults to false. */ public $checkPort = false; + /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * Initializes the validator. */ - public $allowEmpty = true; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is not a valid email address.'); + } + } /** * Validates the attribute of the object. @@ -57,21 +65,15 @@ class EmailValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } if (!$this->validateValue($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } /** - * Validates a static value to see if it is a valid email. - * Note that this method does not respect [[allowEmpty]] property. - * This method is provided so that you can call it directly without going through the model validation rule mechanism. - * @param mixed $value the value to be validated - * @return boolean whether the value is a valid email + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. */ public function validateValue($value) { @@ -98,8 +100,7 @@ class EmailValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -110,7 +111,7 @@ class EmailValidator extends Validator } return " -if(" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { +if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index 8df3e19..7aa434c 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -6,6 +6,8 @@ */ namespace yii\validators; + +use Yii; use yii\base\InvalidConfigException; /** @@ -34,11 +36,18 @@ class ExistValidator extends Validator * @see className */ public $attributeName; + + /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * Initializes the validator. */ - public $allowEmpty = true; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is invalid.'); + } + } /** * Validates the attribute of the object. @@ -46,29 +55,48 @@ class ExistValidator extends Validator * * @param \yii\db\ActiveRecord $object the object being validated * @param string $attribute the attribute being validated - * @throws InvalidConfigException if table doesn't have column specified */ public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + + if (is_array($value)) { + $this->addError($object, $attribute, $this->message); return; } /** @var $className \yii\db\ActiveRecord */ - $className = ($this->className === null) ? get_class($object) : \Yii::import($this->className); - $attributeName = ($this->attributeName === null) ? $attribute : $this->attributeName; - $table = $className::getTableSchema(); - if (($column = $table->getColumn($attributeName)) === null) { - throw new InvalidConfigException('Table "' . $table->name . '" does not have a column named "' . $attributeName . '"'); - } - + $className = $this->className === null ? get_class($object) : Yii::import($this->className); + $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $query = $className::find(); - $query->where(array($column->name => $value)); + $query->where(array($attributeName => $value)); if (!$query->exists()) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} "{value}" is invalid.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + * @throws InvalidConfigException if either [[className]] or [[attributeName]] is not set. + */ + public function validateValue($value) + { + if (is_array($value)) { + return false; + } + if ($this->className === null) { + throw new InvalidConfigException('The "className" property must be set.'); + } + if ($this->attributeName === null) { + throw new InvalidConfigException('The "attributeName" property must be set.'); + } + /** @var $className \yii\db\ActiveRecord */ + $className = $this->className; + $query = $className::find(); + $query->where(array($this->attributeName => $value)); + return $query->exists(); + } } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index b05ac2a..c104c05 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -7,47 +7,19 @@ namespace yii\validators; +use Yii; +use yii\helpers\FileHelper; +use yii\web\UploadedFile; + /** - * CFileValidator verifies if an attribute is receiving a valid uploaded file. - * - * It uses the model class and attribute name to retrieve the information - * about the uploaded file. It then checks if a file is uploaded successfully, - * if the file size is within the limit and if the file type is allowed. - * - * This validator will attempt to fetch uploaded data if attribute is not - * previously set. Please note that this cannot be done if input is tabular: - *
- *  foreach($models as $i=>$model)
- *     $model->attribute = CUploadedFile::getInstance($model, "[$i]attribute");
- * 
- * 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: - *
- *  if($model->save())
- *  {
- *     // single upload
- *     $model->attribute->saveAs($path);
- *     // multiple upload
- *     foreach($model->attribute as $file)
- *        $file->saveAs($path);
- *  }
- * 
- * - * 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(FileHelper::getExtension($file->getName())), $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: - *
    - *
  • 'upload_max_filesize' in php.ini
  • - *
  • 'MAX_FILE_SIZE' hidden field
  • - *
  • {@link maxSize}
  • - *
+ * + * - 'upload_max_filesize' in php.ini + * - 'MAX_FILE_SIZE' hidden field + * - [[maxSize]] * * @return integer the size limit for uploaded files. */ @@ -203,10 +218,12 @@ class CFileValidator extends Validator { $limit = ini_get('upload_max_filesize'); $limit = $this->sizeToBytes($limit); - if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) + if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) { $limit = $this->maxSize; - if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) - $limit = $_POST['MAX_FILE_SIZE']; + } + if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) { + $limit = (int)$_POST['MAX_FILE_SIZE']; + } return $limit; } @@ -218,12 +235,18 @@ class CFileValidator extends Validator */ private function sizeToBytes($sizeStr) { - switch (substr($sizeStr, -1)) - { - case 'M': case 'm': return (int)$sizeStr * 1048576; - case 'K': case 'k': return (int)$sizeStr * 1024; - case 'G': case 'g': return (int)$sizeStr * 1073741824; - default: return (int)$sizeStr; + switch (substr($sizeStr, -1)) { + case 'M': + case 'm': + return (int)$sizeStr * 1048576; + case 'K': + case 'k': + return (int)$sizeStr * 1024; + case 'G': + case 'g': + return (int)$sizeStr * 1073741824; + default: + return (int)$sizeStr; } } } \ No newline at end of file diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index c891979..72a9a9d 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -38,6 +38,23 @@ class FilterValidator extends Validator * ~~~ */ public $filter; + /** + * @var boolean this property is overwritten to be false so that this validator will + * be applied when the value being validated is empty. + */ + public $skipOnEmpty = false; + + /** + * Initializes the validator. + * @throws InvalidConfigException if [[filter]] is not set. + */ + public function init() + { + parent::init(); + if ($this->filter === null) { + throw new InvalidConfigException('The "filter" property must be set.'); + } + } /** * Validates the attribute of the object. @@ -48,9 +65,6 @@ class FilterValidator extends Validator */ public function validateAttribute($object, $attribute) { - if ($this->filter === null) { - throw new InvalidConfigException('The "filter" property must be specified with a valid callback.'); - } $object->$attribute = call_user_func($this->filter, $object->$attribute); } } diff --git a/framework/validators/InlineValidator.php b/framework/validators/InlineValidator.php index 5c12d52..3689a2f 100644 --- a/framework/validators/InlineValidator.php +++ b/framework/validators/InlineValidator.php @@ -25,8 +25,9 @@ namespace yii\validators; class InlineValidator extends Validator { /** - * @var string the name of the validation method defined in the - * \yii\base\Model class + * @var string|\Closure an anonymous function or the name of a model class method that will be + * called to perform the actual validation. Note that if you use anonymous function, you cannot + * use `$this` in it unless you are using PHP 5.4 or above. */ public $method; /** @@ -34,8 +35,8 @@ class InlineValidator extends Validator */ public $params; /** - * @var string the name of the method that returns the client validation code (see [[clientValidateAttribute()]] - * for details on how to return client validation code). The signature of the method should be like the following: + * @var string|\Closure an anonymous function or the name of a model class method that returns the client validation code. + * The signature of the method should be like the following: * * ~~~ * function foo($attribute) @@ -45,6 +46,8 @@ class InlineValidator extends Validator * ~~~ * * where `$attribute` refers to the attribute name to be validated. + * + * Please refer to [[clientValidateAttribute()]] for details on how to return client validation code. */ public $clientValidate; @@ -56,7 +59,10 @@ class InlineValidator extends Validator public function validateAttribute($object, $attribute) { $method = $this->method; - $object->$method($attribute, $this->params); + if (is_string($method)) { + $method = array($object, $method); + } + call_user_func($method, $attribute, $this->params); } /** @@ -82,7 +88,10 @@ class InlineValidator extends Validator { if ($this->clientValidate !== null) { $method = $this->clientValidate; - return $object->$method($attribute); + if (is_string($method)) { + $method = array($object, $method); + } + return call_user_func($method, $attribute); } else { return null; } diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 89363fb..915419e 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -26,11 +26,6 @@ class NumberValidator extends Validator */ public $integerOnly = false; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var integer|float upper limit of the number. Defaults to null, meaning no upper limit. */ public $max; @@ -58,6 +53,24 @@ class NumberValidator extends Validator /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.') + : Yii::t('yii|{attribute} must be a number.'); + } + if ($this->min !== null && $this->tooSmall === null) { + $this->tooSmall = Yii::t('yii|{attribute} must be no less than {min}.'); + } + if ($this->max !== null && $this->tooBig === null) { + $this->tooBig = Yii::t('yii|{attribute} must be no greater than {max}.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -66,31 +79,35 @@ class NumberValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); return; } - if ($this->integerOnly) { - if (!preg_match($this->integerPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be an integer.'); - $this->addError($object, $attribute, $message); - } - } else { - if (!preg_match($this->numberPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be a number.'); - $this->addError($object, $attribute, $message); - } + $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; + if (!preg_match($pattern, "$value")) { + $this->addError($object, $attribute, $this->message); } if ($this->min !== null && $value < $this->min) { - $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii|{attribute} is too small (minimum is {min}).'); - $this->addError($object, $attribute, $message, array('{min}' => $this->min)); + $this->addError($object, $attribute, $this->tooSmall, array('{min}' => $this->min)); } if ($this->max !== null && $value > $this->max) { - $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii|{attribute} is too big (maximum is {max}).'); - $this->addError($object, $attribute, $message, array('{max}' => $this->max)); + $this->addError($object, $attribute, $this->tooBig, array('{max}' => $this->max)); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return preg_match($this->integerOnly ? $this->integerPattern : $this->numberPattern, "$value") + && ($this->min === null || $value >= $this->min) + && ($this->max === null || $value <= $this->max); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. @@ -99,12 +116,7 @@ class NumberValidator extends Validator public function clientValidateAttribute($object, $attribute) { $label = $object->getAttributeLabel($attribute); - - if (($message = $this->message) === null) { - $message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.') - : Yii::t('yii|{attribute} must be a number.'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $label, )); @@ -115,10 +127,7 @@ if(!value.match($pattern)) { } "; if ($this->min !== null) { - if (($tooSmall = $this->tooSmall) === null) { - $tooSmall = Yii::t('yii|{attribute} is too small (minimum is {min}).'); - } - $tooSmall = strtr($tooSmall, array( + $tooSmall = strtr($this->tooSmall, array( '{attribute}' => $label, '{min}' => $this->min, )); @@ -130,10 +139,7 @@ if(value<{$this->min}) { "; } if ($this->max !== null) { - if (($tooBig = $this->tooBig) === null) { - $tooBig = Yii::t('yii|{attribute} is too big (maximum is {max}).'); - } - $tooBig = strtr($tooBig, array( + $tooBig = strtr($this->tooBig, array( '{attribute}' => $label, '{max}' => $this->max, )); @@ -144,7 +150,7 @@ if(value>{$this->max}) { "; } - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if(jQuery.trim(value)!='') { $js diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index e23567c..18742ae 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -6,6 +6,8 @@ */ namespace yii\validators; + +use Yii; use yii\base\InvalidConfigException; /** @@ -29,58 +31,62 @@ class RangeValidator extends Validator */ public $strict = false; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var boolean whether to invert the validation logic. Defaults to false. If set to true, * the attribute value should NOT be among the list of values defined via [[range]]. **/ public $not = false; /** + * Initializes the validator. + * @throws InvalidConfigException if [[range]] is not set. + */ + public function init() + { + parent::init(); + if (!is_array($this->range)) { + throw new InvalidConfigException('The "range" property must be set.'); + } + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is invalid.'); + } + } + + /** * Validates the attribute of the object. * 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 - * @throws InvalidConfigException if the "range" property is not an array */ public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if (!is_array($this->range)) { - throw new InvalidConfigException('The "range" property must be specified as an array.'); - } if (!$this->not && !in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should be in the list.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } elseif ($this->not && in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should NOT be in the list.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return !$this->not && in_array($value, $this->range, $this->strict) + || $this->not && !in_array($value, $this->range, $this->strict); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. * @return string the client-side validation script. - * @throws InvalidConfigException if the "range" property is not an array */ public function clientValidateAttribute($object, $attribute) { - if (!is_array($this->range)) { - throw new InvalidConfigException('The "range" property must be specified as an array.'); - } - - if (($message = $this->message) === null) { - $message = $this->not ? \Yii::t('yii|{attribute} should NOT be in the list.') : \Yii::t('yii|{attribute} should be in the list.'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -92,7 +98,7 @@ class RangeValidator extends Validator $range = json_encode($range); return " -if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? "$.inArray(value, $range)>=0" : "$.inArray(value, $range)<0") . ") { +if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? "$.inArray(value, $range)>=0" : "$.inArray(value, $range)<0") . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index df2b657..6c69be3 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -7,6 +7,9 @@ namespace yii\validators; +use Yii; +use yii\base\InvalidConfigException; + /** * RegularExpressionValidator validates that the attribute value matches the specified [[pattern]]. * @@ -22,53 +25,63 @@ class RegularExpressionValidator extends Validator */ public $pattern; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var boolean whether to invert the validation logic. Defaults to false. If set to true, * the regular expression defined via [[pattern]] should NOT match the attribute value. + * @throws InvalidConfigException if the "pattern" is not a valid regular expression **/ public $not = false; /** + * Initializes the validator. + * @throws InvalidConfigException if [[pattern]] is not set. + */ + public function init() + { + parent::init(); + if ($this->pattern === null) { + throw new InvalidConfigException('The "pattern" property must be set.'); + } + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is invalid.'); + } + } + + /** * Validates the attribute of the object. * 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 - * @throws \yii\base\Exception if the "pattern" is not a valid regular expression */ public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if ($this->pattern === null) { - throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); - } - if ((!$this->not && !preg_match($this->pattern, $value)) || ($this->not && preg_match($this->pattern, $value))) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); - $this->addError($object, $attribute, $message); + if (!$this->validateValue($value)) { + $this->addError($object, $attribute, $this->message); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return !is_array($value) && + (!$this->not && preg_match($this->pattern, $value) + || $this->not && !preg_match($this->pattern, $value)); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. * @return string the client-side validation script. - * @throws \yii\base\Exception if the "pattern" is not a valid regular expression + * @throws InvalidConfigException if the "pattern" is not a valid regular expression */ public function clientValidateAttribute($object, $attribute) { - if ($this->pattern === null) { - throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); - } - - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -88,7 +101,7 @@ class RegularExpressionValidator extends Validator } return " -if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? '' : '!') . "value.match($pattern)) { +if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? '' : '!') . "value.match($pattern)) { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index 66b9c3c..3b13eb3 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * RequiredValidator validates that the specified attribute does not have null or empty value. * @@ -16,6 +18,10 @@ namespace yii\validators; class RequiredValidator extends Validator { /** + * @var boolean whether to skip this validator if the value being validated is empty. + */ + public $skipOnEmpty = false; + /** * @var mixed the desired value that the attribute must have. * If this is null, the validator will validate that the specified attribute is not empty. * If this is set as a value that is not null, the validator will validate that @@ -35,6 +41,18 @@ class RequiredValidator extends Validator public $strict = false; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = $this->requiredValue === null ? Yii::t('yii|{attribute} is invalid.') + : Yii::t('yii|{attribute} must be "{requiredValue}".'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -45,13 +63,11 @@ class RequiredValidator extends Validator $value = $object->$attribute; if ($this->requiredValue === null) { if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } else { if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be "{requiredValue}".'); - $this->addError($object, $attribute, $message, array( + $this->addError($object, $attribute, $this->message, array( '{requiredValue}' => $this->requiredValue, )); } @@ -59,6 +75,23 @@ class RequiredValidator extends Validator } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + if ($this->requiredValue === null) { + if ($this->strict && $value !== null || !$this->strict && !$this->isEmpty($value, true)) { + return true; + } + } elseif (!$this->strict && $value == $this->requiredValue || $this->strict && $value === $this->requiredValue) { + return true; + } + return false; + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. @@ -66,12 +99,8 @@ class RequiredValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = $this->message; if ($this->requiredValue !== null) { - if ($message === null) { - $message = \Yii::t('yii|{attribute} must be "{requiredValue}".'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, '{requiredValue}' => $this->requiredValue, @@ -82,10 +111,7 @@ if (value != " . json_encode($this->requiredValue) . ") { } "; } else { - if ($message === null) { - $message = \Yii::t('yii|{attribute} cannot be blank.'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 9135b9e..8b8c73b 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * StringValidator validates that the attribute value is of certain length. * @@ -46,19 +48,34 @@ class StringValidator extends Validator */ public $notEqual; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * @var string the encoding of the string value to be validated (e.g. 'UTF-8'). + * If this property is not set, [[\yii\base\Application::charset]] will be used. */ - public $allowEmpty = true; + public $encoding; + + /** - * @var mixed the encoding of the string value to be validated (e.g. 'UTF-8'). - * This property is used only when mbstring PHP extension is enabled. - * The value of this property will be used as the 2nd parameter of the - * mb_strlen() function. If this property is not set, the application charset - * will be used. If this property is set false, then strlen() will be used even - * if mbstring is enabled. + * Initializes the validator. */ - public $encoding; + public function init() + { + parent::init(); + if ($this->encoding === null) { + $this->encoding = Yii::$app->charset; + } + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} must be a string.'); + } + if ($this->min !== null && $this->tooShort === null) { + $this->tooShort = Yii::t('yii|{attribute} should contain at least {min} characters.'); + } + if ($this->max !== null && $this->tooLong === null) { + $this->tooLong = Yii::t('yii|{attribute} should contain at most {max} characters.'); + } + if ($this->is !== null && $this->notEqual === null) { + $this->notEqual = Yii::t('yii|{attribute} should contain {length} characters.'); + } + } /** * Validates the attribute of the object. @@ -69,34 +86,39 @@ class StringValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } if (!is_string($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be a string.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); return; } - if (function_exists('mb_strlen') && $this->encoding !== false) { - $length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$app->charset); - } else { - $length = strlen($value); - } + $length = mb_strlen($value, $this->encoding); if ($this->min !== null && $length < $this->min) { - $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); - $this->addError($object, $attribute, $message, array('{min}' => $this->min)); + $this->addError($object, $attribute, $this->tooShort, array('{min}' => $this->min)); } if ($this->max !== null && $length > $this->max) { - $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); - $this->addError($object, $attribute, $message, array('{max}' => $this->max)); + $this->addError($object, $attribute, $this->tooLong, array('{max}' => $this->max)); } if ($this->is !== null && $length !== $this->is) { - $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); - $this->addError($object, $attribute, $message, array('{length}' => $this->is)); + $this->addError($object, $attribute, $this->notEqual, array('{length}' => $this->is)); + } + } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + if (!is_string($value)) { + return false; } + $length = mb_strlen($value, $this->encoding); + return ($this->min === null || $length >= $this->min) + && ($this->max === null || $length <= $this->max) + && ($this->is === null || $length === $this->is); } /** @@ -110,28 +132,19 @@ class StringValidator extends Validator $label = $object->getAttributeLabel($attribute); $value = $object->$attribute; - if (($notEqual = $this->notEqual) === null) { - $notEqual = \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); - } - $notEqual = strtr($notEqual, array( + $notEqual = strtr($this->notEqual, array( '{attribute}' => $label, '{value}' => $value, '{length}' => $this->is, )); - if (($tooShort = $this->tooShort) === null) { - $tooShort = \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); - } - $tooShort = strtr($tooShort, array( + $tooShort = strtr($this->tooShort, array( '{attribute}' => $label, '{value}' => $value, '{min}' => $this->min, )); - if (($tooLong = $this->tooLong) === null) { - $tooLong = \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); - } - $tooLong = strtr($tooLong, array( + $tooLong = strtr($this->tooLong, array( '{attribute}' => $label, '{value}' => $value, '{max}' => $this->max, @@ -160,7 +173,7 @@ if(value.length!= {$this->is}) { "; } - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if($.trim(value)!='') { $js diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index bc12f5a..fa55df7 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -6,6 +6,8 @@ */ namespace yii\validators; + +use Yii; use yii\base\InvalidConfigException; /** @@ -17,11 +19,6 @@ use yii\base\InvalidConfigException; class UniqueValidator extends Validator { /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var string the ActiveRecord class name or alias of the class * that should be used to look for the attribute value being validated. * Defaults to null, meaning using the ActiveRecord class of the attribute being validated. @@ -36,6 +33,17 @@ class UniqueValidator extends Validator public $attributeName; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} "{value}" has already been taken.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\db\ActiveRecord $object the object being validated @@ -45,7 +53,9 @@ class UniqueValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); return; } @@ -55,7 +65,7 @@ class UniqueValidator extends Validator $table = $className::getTableSchema(); if (($column = $table->getColumn($attributeName)) === null) { - throw new InvalidConfigException('Table "' . $table->name . '" does not have a column named "' . $attributeName . '"'); + throw new InvalidConfigException("Table '{$table->name}' does not have a column named '$attributeName'."); } $query = $className::find(); @@ -84,8 +94,7 @@ class UniqueValidator extends Validator } if ($exists) { - $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} "{value}" has already been taken.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } } \ No newline at end of file diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index 0ba039b..cd6bfef 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * UrlValidator validates that the attribute value is a valid http or https URL. * @@ -32,11 +34,18 @@ class UrlValidator extends Validator * contain the scheme part. **/ public $defaultScheme; + + /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * Initializes the validator. */ - public $allowEmpty = true; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is not a valid URL.'); + } + } /** * Validates the attribute of the object. @@ -47,23 +56,19 @@ class UrlValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if (($value = $this->validateValue($value)) !== false) { - $object->$attribute = $value; + if ($this->validateValue($value)) { + if ($this->defaultScheme !== null && strpos($value, '://') === false) { + $object->$attribute = $this->defaultScheme . '://' . $value; + } } else { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } /** - * Validates a static value to see if it is a valid URL. - * Note that this method does not respect [[allowEmpty]] property. - * This method is provided so that you can call it directly without going through the model validation rule mechanism. - * @param mixed $value the value to be validated - * @return mixed false if the the value is not a valid URL, otherwise the possibly modified value ({@see defaultScheme}) + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. */ public function validateValue($value) { @@ -80,7 +85,7 @@ class UrlValidator extends Validator } if (preg_match($pattern, $value)) { - return $value; + return true; } } return false; @@ -95,8 +100,7 @@ class UrlValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -121,7 +125,7 @@ $js "; } - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if($.trim(value)!='') { $js diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index b688f32..5ab8dfe 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -7,7 +7,9 @@ namespace yii\validators; +use Yii; use yii\base\Component; +use yii\base\NotSupportedException; /** * Validator is the base class for all validators. @@ -81,7 +83,7 @@ abstract class Validator extends Component */ public $message; /** - * @var array list of scenarios that the validator should be applied. + * @var array list of scenarios that the validator can be applied to. */ public $on = array(); /** @@ -94,6 +96,12 @@ abstract class Validator extends Component */ public $skipOnError = true; /** + * @var boolean whether this validation rule should be skipped if the attribute value + * is null or an empty string. + */ + public $skipOnEmpty = true; + + /** * @var boolean whether to enable client-side validation. Defaults to null, meaning * its actual value inherits from that of [[\yii\web\ActiveForm::enableClientValidation]]. */ @@ -149,7 +157,7 @@ abstract class Validator extends Component } } - return \Yii::createObject($params); + return Yii::createObject($params); } /** @@ -168,13 +176,26 @@ abstract class Validator extends Component $attributes = $this->attributes; } foreach ($attributes as $attribute) { - if (!($this->skipOnError && $object->hasErrors($attribute))) { + $skip = $this->skipOnError && $object->hasErrors($attribute) + || $this->skipOnEmpty && $this->isEmpty($object->$attribute); + if (!$skip) { $this->validateAttribute($object, $attribute); } } } /** + * Validates a value. + * A validator class can implement this method to support data validation out of the context of a data model. + * @param mixed $value the data value to be validated. + * @throws NotSupportedException if data validation without a model is not supported + */ + public function validateValue($value) + { + throw new NotSupportedException(get_class($this) . ' does not support validateValue().'); + } + + /** * Returns the JavaScript needed for performing client-side validation. * * You may override this method to return the JavaScript validation code if @@ -224,8 +245,9 @@ abstract class Validator extends Component */ public function addError($object, $attribute, $message, $params = array()) { + $value = $object->$attribute; $params['{attribute}'] = $object->getAttributeLabel($attribute); - $params['{value}'] = $object->$attribute; + $params['{value}'] = is_array($value) ? 'array()' : $value; $object->addError($attribute, strtr($message, $params)); } diff --git a/framework/web/Application.php b/framework/web/Application.php index b839d92..f9b615d 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -23,21 +23,15 @@ class Application extends \yii\base\Application public $defaultRoute = 'site'; /** - * Sets default path aliases. - */ - public function registerDefaultAliases() - { - parent::registerDefaultAliases(); - Yii::$aliases['@webroot'] = dirname($_SERVER['SCRIPT_FILENAME']); - } - - /** * Processes the request. * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) */ public function processRequest() { - list ($route, $params) = $this->getRequest()->resolve(); + $request = $this->getRequest(); + Yii::setAlias('@wwwroot', dirname($request->getScriptFile())); + Yii::setAlias('@www', $request->getBaseUrl()); + list ($route, $params) = $request->resolve(); return $this->runAction($route, $params); } 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/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/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 8ac5365..48bc181 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -52,10 +52,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 @@ -240,35 +236,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 + 1); - } - 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 getInputId($model, $attribute) - { - $name = $this->getInputName($model, $attribute); - return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name); - } - public function getAttributeValue($model, $attribute) { if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { @@ -299,4 +266,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/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/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index 2c3de72..bf0ca0a 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -10,7 +10,9 @@ class HtmlTest extends \yii\test\TestCase { public function setUp() { - new Application('test', '@yiiunit/runtime', array( + new Application(array( + 'id' => 'test', + 'basePath' => '@yiiunit/runtime', 'components' => array( 'request' => array( 'class' => 'yii\web\Request',