diff --git a/docs/guide/db-query-builder.md b/docs/guide/db-query-builder.md index b997a65..c87c795 100644 --- a/docs/guide/db-query-builder.md +++ b/docs/guide/db-query-builder.md @@ -1,65 +1,88 @@ -Query Builder and Query -======================= +Query Builder +============= > Note: This section is under development. -Yii provides a basic database access layer as described in the [Database basics](db-dao.md) section. -The database access layer provides a low-level way to interact with the database. While useful in some situations, -it can be tedious and error-prone to write raw SQLs. An alternative approach is to use the Query Builder. -The Query Builder provides an object-oriented vehicle for generating queries to be executed. +Built on top of [Database Access Objects](db-dao.md), query builder allows you to construct a SQL statement +in a programmatic way. Compared to writing raw SQLs, using query builder will help you write more readable +SQL-related code and generate more secure SQL statements. -A typical usage of the query builder looks like the following: +Using query builder usually involves two steps: + +1. Build a [[yii\db\Query]] object to represent different parts (e.g. `SELECT`, `FROM`) of a SELECT SQL statement. +2. Execute a query method (e.g. `all()`) of [[yii\db\Query]]. + +Behind the scene, [[yii\db\QueryBuilder]] is invoked in the second step to convert a [[yii\db\Query]] object into +a SQL statement for the particular DBMS. + +The following code shows a typical use case of query builder: ```php $rows = (new \yii\db\Query()) - ->select('id, name') + ->select('id, email') ->from('user') + ->where(['name' => 'Smith']) ->limit(10) ->all(); +``` -// which is equivalent to the following code: - -$query = (new \yii\db\Query()) - ->select('id, name') - ->from('user') - ->limit(10); - -// Create a command. You can get the actual SQL using $command->sql -$command = $query->createCommand(); +It generates and executes the following SQL statement: -// Execute the command: -$rows = $command->queryAll(); +```sql +SELECT `id`, `email` +FROM `user` +WHERE `name` = :name +LIMIT 10 ``` -Query Methods -------------- +where the `:name` parameter is bound with the string `'Smith'`. Depending on the DBMS being used, query builder +will properly quote the column and table names and bind parameter values to the generated SQL statement. + -As you can see, [[yii\db\Query]] is the main player that you need to deal with. Behind the scenes, -`Query` is actually only responsible for representing various query information. The actual query -building logic is done by [[yii\db\QueryBuilder]] when you call the `createCommand()` method, -and the query execution is done by [[yii\db\Command]]. +## Query Methods -For convenience, [[yii\db\Query]] provides a set of commonly used query methods that will build -the query, execute it, and return the result. For example, +[[yii\db\Query]] provides a whole set of methods for different query purposes: -- [[yii\db\Query::all()|all()]]: builds the query, executes it and returns all results as an array. +- [[yii\db\Query::all()|all()]]: returns an array of rows with each row being an associative array of name-value pairs. - [[yii\db\Query::one()|one()]]: returns the first row of the result. - [[yii\db\Query::column()|column()]]: returns the first column of the result. -- [[yii\db\Query::scalar()|scalar()]]: returns the first column in the first row of the result. -- [[yii\db\Query::exists()|exists()]]: returns a value indicating whether the query results in anything. -- [[yii\db\Query::count()|count()]]: returns the result of a `COUNT` query. Other similar methods - include `sum($q)`, `average($q)`, `max($q)` and `min($q)`, which support the so-called aggregational data query. The `$q` - parameter is mandatory for these methods and can be either the column name or expression. +- [[yii\db\Query::scalar()|scalar()]]: returns a scalar value located at the first row and first column of the result. +- [[yii\db\Query::exists()|exists()]]: returns a value indicating whether the query contains any result. +- [[yii\db\Query::count()|count()]]: returns the result of a `COUNT` query. +- Other aggregation query methods, including [[yii\db\Query::sum()|sum($q)]], [[yii\db\Query::average()|average($q)]], + [[yii\db\Query::max()|max($q)]], [[yii\db\Query::min()|min($q)]]. The `$q` parameter is mandatory for these methods + and can be either a column name or a DB expression. +All of the above methods take an optional `$db` parameter representing the [[yii\db\Connection|DB connection]] that +should be used to perform a DB query. If you omit this parameter, the `db` application component will be used +as the DB connection. -Building Query --------------- +Given a [[yii\db\Query]], you can create a [[yii\db\Command]] and further work with this command object. For example, -In the following, we will explain how to build various clauses in a SQL statement. For simplicity, -we use `$query` to represent a [[yii\db\Query]] object. +```php +$command = (new \yii\db\Query()) + ->select('id, email') + ->from('user') + ->where(['name' => 'Smith']) + ->limit(10) + ->createCommand(); + +// show the SQL statement +echo $command->sql; +// returns all rows of the query result +$rows= $command->queryAll(); +``` -### `SELECT` +## Building Queries + +To build a [[yii\db\Query]] object, you call different query building methods to specify different parts of +a SQL statement. The names of these methods resemble the SQL keywords used in the corresponding parts of the SQL +statement. For example, to specify the `FROM` part, you would call the `from()` method. In the following, we will +describe in detail the usage of each query building method. + + +### `SELECT` In order to form a basic `SELECT` query, you need to specify what columns to select and from what table: @@ -100,7 +123,7 @@ To select distinct rows, you may call `distinct()`, like the following: $query->select('user_id')->distinct()->from('post'); ``` -### `FROM` +### `FROM` To specify which table(s) to select data from, call `from()`: @@ -133,7 +156,7 @@ $query->select('*')->from(['u' => $subQuery]); ``` -### `WHERE` +### `WHERE` Usually data is selected based upon certain criteria. Query Builder has some useful methods to specify these, the most powerful of which being `where`. It can be used in multiple ways. @@ -321,7 +344,7 @@ A value is *empty* if it is null, an empty string, a string consisting of whites You may also use `andFilterWhere()` and `orFilterWhere()` to append more filter conditions. -### `ORDER BY` +### `ORDER BY` For ordering results `orderBy` and `addOrderBy` could be used: @@ -420,9 +443,11 @@ $anotherQuery->select('id, type, name')->from('user')->limit(10); $query->union($anotherQuery); ``` +## Indexing Query Results + +TBD -Batch Query ------------ +## Batch Query When working with large amounts of data, methods such as [[yii\db\Query::all()]] are not suitable because they require loading all data into the memory. To keep the memory requirement low, Yii diff --git a/docs/guide/input-file-upload.md b/docs/guide/input-file-upload.md index 76f386e..eb89dc6 100644 --- a/docs/guide/input-file-upload.md +++ b/docs/guide/input-file-upload.md @@ -141,18 +141,24 @@ It is wise to validate the type of file uploaded. FileValidator has the property public function rules() { return [ - [['file'], 'file', 'extensions' => 'gif, jpg',], + [['file'], 'file', 'extensions' => 'gif, jpg'], ]; } ``` -Keep in mind that only the file extension will be validated, but not the actual file content. In order to validate the content as well, use the `mimeTypes` property of `FileValidator`: +By default it will validate against file content mime type corresponding to extension specified. For gif it will be +`image/gif`, for `jpg` it will be `image/jpeg`. + + +Note that some mime types can't be detected properly by PHP's fileinfo extension that is used by `file` validator. For +example, `csv` files are detected as `text/plain` instead of `text/csv`. You can turn off such behavior by setting +`checkExtensionByMimeType` to `false` and specifying mime types manually: ```php public function rules() { return [ - [['file'], 'file', 'extensions' => 'jpg, png', 'mimeTypes' => 'image/jpeg, image/png',], + [['file'], 'file', 'checkExtensionByMimeType' => false, 'extensions' => 'csv', 'mimeTypes' => 'text/plain'], ]; } ``` diff --git a/docs/guide/runtime-routing.md b/docs/guide/runtime-routing.md index 5283dc8..2745a65 100644 --- a/docs/guide/runtime-routing.md +++ b/docs/guide/runtime-routing.md @@ -42,7 +42,7 @@ The [[yii\web\UrlManager|URL manager]] supports two URL formats: the default URL The default URL format uses a query parameter named `r` to represent the route and normal query parameters to represent the query parameters associated with the route. For example, the URL `/index.php?r=post/view&id=100` represents -the route `post/view` and the `id` query parameter 100. The default URL format does not require any configuration about +the route `post/view` and the `id` query parameter 100. The default URL format does not require any configuration of the [[yii\web\UrlManager|URL manager]] and works in any Web server setup. The pretty URL format uses the extra path following the entry script name to represent the route and the associated @@ -275,7 +275,7 @@ The rest of the properties are optional. However, their configuration shown abov A URL rule is an instance of [[yii\web\UrlRule]] or its child class. Each URL rule consists of a pattern used for matching the path info part of URLs, a route, and a few query parameters. A URL rule can be used to parse a request -if its pattern matches the requested URL and a URL rule can be used to create a URL if its route and query parameter +if its pattern matches the requested URL. A URL rule can be used to create a URL if its route and query parameter names match those that are given. When the pretty URL format is enabled, the [[yii\web\UrlManager|URL manager]] uses the URL rules declared in its diff --git a/docs/guide/start-installation.md b/docs/guide/start-installation.md index 265e30b..6897bcc 100644 --- a/docs/guide/start-installation.md +++ b/docs/guide/start-installation.md @@ -185,7 +185,7 @@ server { root /path/to/basic/web; index index.php; - access_log /path/to/basic/log/access.log main; + access_log /path/to/basic/log/access.log; error_log /path/to/basic/log/error.log; location / { diff --git a/docs/internals/versions.md b/docs/internals/versions.md index 6e25d38..12730f5 100644 --- a/docs/internals/versions.md +++ b/docs/internals/versions.md @@ -1,10 +1,22 @@ Yii Versioning ============== -This document summarizes the versioning policy of Yii. In general, Yii follows the [Semantic Versioning](http://semver.org/). +This document summarizes the versioning policy of Yii. Our current versioning strategy can be +described as [ferver](https://github.com/jonathanong/ferver), which we considered is more practical +and reasonable than [Semantic Versioning](http://semver.org/) (See [#7408](https://github.com/yiisoft/yii2/issues/7408) for more references). + +Within the core developer team, we have emphasized several times that it is important to keep 2.0.x releases 100% BC-compatible. +But this is an ideal plan. The ferver article has given out a real world example that this is hard to achieve in practice, +regardless you are using semver or not. + +In summary, our versioning policy is as follows: ## Patch Releases `2.x.Y` - + +Patch releases, which should be 100% BC-compatible. Ideally, we hope they contain bug fixes only so that it reduces +the chance of breaking BC. Practically, since 2.0.x is released more frequently, we are also adding minor features +to it so that users can enjoy them earlier. + * Maintained on a branch named `2.x` * Mainly contain bug fixes and minor feature enhancements * No major features. @@ -16,6 +28,9 @@ This document summarizes the versioning policy of Yii. In general, Yii follows t ## Minor Releases `2.X.0` +BC-breaking releases, which contains major features and changes that may break BC. Upgrading from earlier versions may +not be trivial, but a complete upgrade guide or even script will be available. + * Developed on master branch * Mainly contain new features and bug fixes * Contain minor features and bug fixes merged from patch releases @@ -27,4 +42,5 @@ This document summarizes the versioning policy of Yii. In general, Yii follows t ## Major Releases `X.0.0` -None in plan. +It's like 2.0 over 1.0. We expect this only happens every 3 to 5 years, depending on external technology advancement +(such as PHP upgraded from 5.0 to 5.4). diff --git a/extensions/jui/InputWidget.php b/extensions/jui/InputWidget.php index fdff0a4..eaee003 100644 --- a/extensions/jui/InputWidget.php +++ b/extensions/jui/InputWidget.php @@ -8,7 +8,6 @@ namespace yii\jui; use yii\base\Model; -use yii\base\InvalidConfigException; use yii\helpers\Html; /** diff --git a/extensions/jui/README.md b/extensions/jui/README.md index bd2a0a8..7917f44 100644 --- a/extensions/jui/README.md +++ b/extensions/jui/README.md @@ -30,7 +30,7 @@ to the require section of your `composer.json` file. Usage ----- -Fhe following +The following single line of code in a view file would render a [JQuery UI DatePicker](http://api.jqueryui.com/datepicker/) widget: ```php diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 5bb53cb..24d44fa 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,7 +4,10 @@ Yii Framework 2 Change Log 2.0.4 under development ----------------------- -- no changes in this release. +- Bug #7529: Fixed `yii\web\Response::sendContentAsFile()` that was broken in 2.0.3 (samdark) +- Enh #7488: Added `StringHelper::explode` to perform explode with trimming and skipping of empty elements (SilverFire, nineinchnick, creocoder, samdark) +- Enh #7562: `yii help` now lists all sub-commands by default (callmez) +- Enh: Added `yii\helper\Console::wrapText()` method to wrap indented text by console window width and used it in `yii help` command (cebe) 2.0.3 March 01, 2015 diff --git a/framework/base/Component.php b/framework/base/Component.php index 72ea35c..3039fa6 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -199,16 +199,18 @@ class Component extends Object } /** - * Checks if a property value is null. + * Checks if a property is set, i.e. defined and not null. * This method will check in the following order and act accordingly: * - * - a property defined by a setter: return whether the property value is null - * - a property of a behavior: return whether the property value is null + * - a property defined by a setter: return whether the property is set + * - a property of a behavior: return whether the property is set + * - return `false` for non existing properties * * Do not call this method directly as it is a PHP magic method that * will be implicitly called when executing `isset($component->property)`. * @param string $name the property name or the event name - * @return boolean whether the named property is null + * @return boolean whether the named property is set + * @see http://php.net/manual/en/function.isset.php */ public function __isset($name) { @@ -238,6 +240,7 @@ class Component extends Object * will be implicitly called when executing `unset($component->property)`. * @param string $name the property name * @throws InvalidCallException if the property is read only. + * @see http://php.net/manual/en/function.unset.php */ public function __unset($name) { diff --git a/framework/base/Object.php b/framework/base/Object.php index 9b7efe2..0545711 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -163,7 +163,7 @@ class Object implements Configurable } /** - * Checks if the named property is set (not null). + * Checks if a property is set, i.e. defined and not null. * * Do not call this method directly as it is a PHP magic method that * will be implicitly called when executing `isset($object->property)`. @@ -171,6 +171,7 @@ class Object implements Configurable * Note that if the property is not defined, false will be returned. * @param string $name the property name or the event name * @return boolean whether the named property is set (not null). + * @see http://php.net/manual/en/function.isset.php */ public function __isset($name) { @@ -192,6 +193,7 @@ class Object implements Configurable * If the property is read-only, it will throw an exception. * @param string $name the property name * @throws InvalidCallException if the property is read only. + * @see http://php.net/manual/en/function.unset.php */ public function __unset($name) { diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index c24cf97..977a596 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -187,14 +187,56 @@ class HelpController extends Controller $this->stdout("\nThe following commands are available:\n\n", Console::BOLD); $len = 0; foreach ($commands as $command => $description) { - if (($l = strlen($command)) > $len) { + $result = Yii::$app->createController($command); + if ($result !== false) { + /** @var $controller Controller */ + list($controller, $actionID) = $result; + $actions = $this->getActions($controller); + if (!empty($actions)) { + $prefix = $controller->getUniqueId(); + foreach ($actions as $action) { + $string = $prefix . '/' . $action; + if ($action === $controller->defaultAction) { + $string .= ' (default)'; + } + if (($l = strlen($string)) > $len) { + $len = $l; + } + } + } + } elseif (($l = strlen($command)) > $len) { $len = $l; } } foreach ($commands as $command => $description) { - $this->stdout("- " . $this->ansiFormat($command, Console::FG_YELLOW)); - $this->stdout(str_repeat(' ', $len + 3 - strlen($command)) . $description); + $this->stdout('- ' . $this->ansiFormat($command, Console::FG_YELLOW)); + $this->stdout(str_repeat(' ', $len + 4 - strlen($command))); + $this->stdout(Console::wrapText($description, $len + 4 + 2), Console::BOLD); $this->stdout("\n"); + + $result = Yii::$app->createController($command); + if ($result !== false) { + list($controller, $actionID) = $result; + $actions = $this->getActions($controller); + if (!empty($actions)) { + $prefix = $controller->getUniqueId(); + foreach ($actions as $action) { + $string = ' ' . $prefix . '/' . $action; + $this->stdout(' ' . $this->ansiFormat($string, Console::FG_GREEN)); + if ($action === $controller->defaultAction) { + $string .= ' (default)'; + $this->stdout(' (default)', Console::FG_YELLOW); + } + $summary = $controller->getActionHelpSummary($controller->createAction($action)); + if ($summary !== '') { + $this->stdout(str_repeat(' ', $len + 4 - strlen($string))); + $this->stdout(Console::wrapText($summary, $len + 4 + 2)); + } + $this->stdout("\n"); + } + } + $this->stdout("\n"); + } } $scriptName = $this->getScriptName(); $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD); @@ -223,14 +265,24 @@ class HelpController extends Controller if (!empty($actions)) { $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD); $prefix = $controller->getUniqueId(); + + $maxlen = 5; + foreach ($actions as $action) { + $len = strlen($prefix.'/'.$action) + 2 + ($action === $controller->defaultAction ? 10 : 0); + if ($maxlen < $len) { + $maxlen = $len; + } + } foreach ($actions as $action) { $this->stdout('- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW)); + $len = strlen($prefix.'/'.$action) + 2; if ($action === $controller->defaultAction) { $this->stdout(' (default)', Console::FG_GREEN); + $len += 10; } $summary = $controller->getActionHelpSummary($controller->createAction($action)); if ($summary !== '') { - $this->stdout(': ' . $summary); + $this->stdout(str_repeat(' ', $maxlen - $len + 2) . Console::wrapText($summary, $maxlen + 2)); } $this->stdout("\n"); } diff --git a/framework/helpers/BaseConsole.php b/framework/helpers/BaseConsole.php index 366cee1..b12e852 100644 --- a/framework/helpers/BaseConsole.php +++ b/framework/helpers/BaseConsole.php @@ -636,6 +636,46 @@ class BaseConsole } /** + * Word wrap text with indentation to fit the screen size + * + * If screen size could not be detected, or the indentation is greater than the screen size, the text will not be wrapped. + * + * The first line will **not** be indented, so `Console::wrapText("Lorem ipsum dolor sit amet.", 4)` will result in the + * following output, given the screen width is 16 characters: + * + * ``` + * Lorem ipsum + * dolor sit + * amet. + * ``` + * + * @param string $text the text to be wrapped + * @param integer $indent number of spaces to use for indentation. + * @param boolean $refresh whether to force refresh of screen size. + * This will be passed to [[getScreenSize()]]. + * @return string the wrapped text. + * @since 2.0.3 + */ + public static function wrapText($text, $indent = 0, $refresh = false) + { + $size = static::getScreenSize($refresh); + if ($size === false || $size[0] <= $indent) { + return $text; + } + $pad = str_repeat(' ', $indent); + $lines = explode("\n", wordwrap($text, $size[0] - $indent, "\n", true)); + $first = true; + foreach($lines as $i => $line) { + if ($first) { + $first = false; + continue; + } + $lines[$i] = $pad . $line; + } + return implode("\n", $lines); + } + + /** * Gets input from STDIN and returns a string right-trimmed for EOLs. * * @param boolean $raw If set to true, returns the raw string without trimming diff --git a/framework/helpers/BaseHtml.php b/framework/helpers/BaseHtml.php index ca0d785..49dd28b 100644 --- a/framework/helpers/BaseHtml.php +++ b/framework/helpers/BaseHtml.php @@ -839,7 +839,7 @@ class BaseHtml * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. * This option is ignored if `item` option is set. * - separator: string, the HTML code that separates items. - * - itemOptions: array, the options for generating the radio button tag using [[checkbox()]]. + * - itemOptions: array, the options for generating the checkbox tag using [[checkbox()]]. * - item: callable, a callback that can be used to customize the generation of the HTML code * corresponding to a single item in $items. The signature of this callback must be: * @@ -903,8 +903,10 @@ class BaseHtml * @param string|array $selection the selected value(s). * @param array $items the data item used to generate the radio buttons. * The array keys are the radio button values, while the array values are the corresponding labels. - * @param array $options options (name => config) for the radio button list. The following options are supported: + * @param array $options options (name => config) for the radio button list container tag. + * The following options are specially handled: * + * - tag: string, the tag name of the container element. * - unselect: string, the value that should be submitted when none of the radio buttons is selected. * By setting this option, a hidden input will be generated. * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. @@ -1287,7 +1289,6 @@ class BaseHtml * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * * See [[renderTagAttributes()]] for details on how attributes are being rendered. * * @return string the generated radio button tag @@ -1473,12 +1474,17 @@ class BaseHtml * @param array $items the data item used to generate the checkboxes. * The array keys are the checkbox values, and the array values are the corresponding labels. * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the checkbox list. The following options are specially handled: + * @param array $options options (name => config) for the checkbox list container tag. + * The following options are specially handled: * + * - tag: string, the tag name of the container element. * - unselect: string, the value that should be submitted when none of the checkboxes is selected. * You may set this option to be null to prevent default value submission. * If this option is not set, an empty string will be submitted. + * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. + * This option is ignored if `item` option is set. * - separator: string, the HTML code that separates items. + * - itemOptions: array, the options for generating the checkbox tag using [[checkbox()]]. * - item: callable, a callback that can be used to customize the generation of the HTML code * corresponding to a single item in $items. The signature of this callback must be: * @@ -1509,12 +1515,17 @@ class BaseHtml * @param array $items the data item used to generate the radio buttons. * The array keys are the radio values, and the array values are the corresponding labels. * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the radio button list. The following options are specially handled: + * @param array $options options (name => config) for the radio button list container tag. + * The following options are specially handled: * + * - tag: string, the tag name of the container element. * - unselect: string, the value that should be submitted when none of the radio buttons is selected. * You may set this option to be null to prevent default value submission. * If this option is not set, an empty string will be submitted. + * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. + * This option is ignored if `item` option is set. * - separator: string, the HTML code that separates items. + * - itemOptions: array, the options for generating the radio button tag using [[radio()]]. * - item: callable, a callback that can be used to customize the generation of the HTML code * corresponding to a single item in $items. The signature of this callback must be: * diff --git a/framework/helpers/BaseStringHelper.php b/framework/helpers/BaseStringHelper.php index f9a4607..3e3e683 100644 --- a/framework/helpers/BaseStringHelper.php +++ b/framework/helpers/BaseStringHelper.php @@ -234,4 +234,35 @@ class BaseStringHelper return mb_strtolower(mb_substr($string, -$bytes, null, '8bit'), Yii::$app->charset) === mb_strtolower($with, Yii::$app->charset); } } + + /** + * Explodes string into array, optionally trims values and skips empty ones + * + * @param string $string String to be exploded. + * @param string $delimiter Delimiter. Default is ','. + * @param mixed $trim Whether to trim each element. Can be: + * - boolean - to trim normally; + * - string - custom characters to trim. Will be passed as a second argument to `trim()` function. + * - callable - will be called for each value instead of trim. Takes the only argument - value. + * @param boolean $skipEmpty Whether to skip empty strings between delimiters. Default is false. + * @return array + */ + public static function explode($string, $delimiter = ',', $trim = true, $skipEmpty = false) { + $result = explode($delimiter, $string); + if ($trim) { + if ($trim === true) { + $trim = 'trim'; + } elseif (!is_callable($trim)) { + $trim = function($v) use ($trim) { + return trim($v, $trim); + }; + } + $result = array_map($trim, $result); + } + if ($skipEmpty) { + // Wrapped with array_values to make array keys sequential after empty values removing + $result = array_values(array_filter($result)); + } + return $result; + } } diff --git a/framework/messages/el/yii.php b/framework/messages/el/yii.php index ce53d7b..5edd152 100644 --- a/framework/messages/el/yii.php +++ b/framework/messages/el/yii.php @@ -17,49 +17,9 @@ * NOTE: this file must be saved in UTF-8 encoding. */ return [ - 'Are you sure you want to delete this item?' => 'Είστε σίγουροι για τη διαγραφή του αντικειμένου;', - 'Only files with these MIME types are allowed: {mimeTypes}.' => 'Επιτρέπονται μόνο αρχεία με τους ακόλουθους τύπους MIME: {mimeTypes}.', - 'The requested view "{name}" was not found.' => 'Δε βρέθηκε η αιτούμενη όψη "{name}".', - 'in {delta, plural, =1{a day} other{# days}}' => 'σε {delta, plural, =1{μία ημέρα} other{# ημέρες}}', - 'in {delta, plural, =1{a minute} other{# minutes}}' => 'σε {delta, plural, =1{ένα λεπτό} other{# λεπτά}}', - 'in {delta, plural, =1{a month} other{# months}}' => 'σε {delta, plural, =1{ένα μήνα} other{# μήνες}}', - 'in {delta, plural, =1{a second} other{# seconds}}' => 'σε {delta, plural, =1{ένα δευτερόλεπτο} other{# δευτερόλεπτα}}', - 'in {delta, plural, =1{a year} other{# years}}' => 'σε {delta, plural, =1{ένα έτος} other{# έτη}}', - 'in {delta, plural, =1{an hour} other{# hours}}' => 'σε {delta, plural, =1{μία ώρα} other{# ώρες}}', - 'just now' => 'μόλις τώρα', - '{delta, plural, =1{a day} other{# days}} ago' => 'πριν {delta, plural, =1{μία ημέρα} other{# ημέρες}}', - '{delta, plural, =1{a minute} other{# minutes}} ago' => 'πριν {delta, plural, =1{ένα λεπτό} other{# λεπτά}}', - '{delta, plural, =1{a month} other{# months}} ago' => 'πριν {delta, plural, =1{ένα μήνα} other{# μήνες}}', - '{delta, plural, =1{a second} other{# seconds}} ago' => 'πριν {delta, plural, =1{ένα δευτερόλεπτο} other{# δευτερόλεπτα}}', - '{delta, plural, =1{a year} other{# years}} ago' => 'πριν {delta, plural, =1{ένα έτος} other{# έτη}}', - '{delta, plural, =1{an hour} other{# hours}} ago' => 'πριν {delta, plural, =1{μία ώρα} other{# ώρες}}', - '{nFormatted} B' => '{nFormatted} B', - '{nFormatted} GB' => '{nFormatted} GB', - '{nFormatted} GiB' => '{nFormatted} GiB', - '{nFormatted} KB' => '{nFormatted} KB', - '{nFormatted} KiB' => '{nFormatted} KiB', - '{nFormatted} MB' => '{nFormatted} MB', - '{nFormatted} MiB' => '{nFormatted} MiB', - '{nFormatted} PB' => '{nFormatted} PB', - '{nFormatted} PiB' => '{nFormatted} PiB', - '{nFormatted} TB' => '{nFormatted} TB', - '{nFormatted} TiB' => '{nFormatted} TiB', - '{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} {n, plural, =1{byte} other{bytes}}', - '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', - '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', - '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}' => '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', - '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}' => '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', - '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}' => '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', - '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}' => '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', - '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}' => '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', - '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', - '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', - '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', - 'No help for unknown command "{command}".' => 'Δεν υπάρχει βοήθεια για την άγνωστη εντολή "{command}".', - 'No help for unknown sub-command "{command}".' => 'Δεν υπάρχει βοήθεια για την άγνωστη υπό-εντολή "{command}".', - 'Unknown command "{command}".' => 'Άγνωστη εντολή "{command}".', '(not set)' => '(μη ορισμένο)', 'An internal server error occurred.' => 'Υπήρξε ένα εσωτερικό σφάλμα του διακομιστή.', + 'Are you sure you want to delete this item?' => 'Είστε σίγουροι για τη διαγραφή του αντικειμένου;', 'Delete' => 'Διαγραφή', 'Error' => 'Σφάλμα', 'File upload failed.' => 'Η μεταφόρτωση απέτυχε.', @@ -70,6 +30,7 @@ return [ 'Missing required parameters: {params}' => 'Απουσιάζουν απαραίτητες παράμετροι: {params}', 'No' => 'Όχι', 'No results found.' => 'Δε βρέθηκαν αποτελέσματα.', + 'Only files with these MIME types are allowed: {mimeTypes}.' => 'Επιτρέπονται μόνο αρχεία με τους ακόλουθους τύπους MIME: {mimeTypes}.', 'Only files with these extensions are allowed: {extensions}.' => 'Επιτρέπονται αρχεία μόνο με καταλήξεις: {extensions}.', 'Page not found.' => 'Η σελίδα δε βρέθηκε.', 'Please fix the following errors:' => 'Παρακαλώ διορθώστε τα παρακάτω σφάλματα:', @@ -83,6 +44,7 @@ return [ 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Η εικόνα «{file}» είναι πολύ μεγάλη. Το πλάτος δεν μπορεί να είναι μεγαλύτερο από {limit, number} {limit, plural, one{pixel} few{pixels} many{pixels} other{pixels}}.', 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Η εικόνα «{file}» είναι πολύ μικρή. To ύψος δεν μπορεί να είναι μικρότερο από {limit, number} {limit, plural, one{pixel} few{pixels} many{pixels} other{pixels}}.', 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Η εικόνα «{file}» είναι πολύ μικρή. Το πλάτος του δεν μπορεί να είναι μικρότερο από {limit, number} {limit, plural, one{pixel} few{pixels} many{pixels} other{pixels}}.', + 'The requested view "{name}" was not found.' => 'Δε βρέθηκε η αιτούμενη όψη "{name}".', 'The verification code is incorrect.' => 'Ο κωδικός επαλήθευσης είναι εσφαλμένος.', 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Συνολικά {count, number} {count, plural, one{αντικείμενο} few{αντικείμενα} many{αντικείμενα} other{αντικείμενα}}.', 'Unable to verify your data submission.' => 'Δεν ήταν δυνατή η επαλήθευση των απεσταλμένων δεδομένων.', @@ -92,6 +54,13 @@ return [ 'Yes' => 'Ναι', 'You are not allowed to perform this action.' => 'Δεν επιτρέπεται να εκτελέσετε αυτή την ενέργεια.', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Μπορείτε να ανεβάσετε το πολύ {limit, number} {limit, plural, one{αρχείο} few{αρχεία} many{αρχεία} other{αρχεία}}.', + 'in {delta, plural, =1{a day} other{# days}}' => 'σε {delta, plural, =1{μία ημέρα} other{# ημέρες}}', + 'in {delta, plural, =1{a minute} other{# minutes}}' => 'σε {delta, plural, =1{ένα λεπτό} other{# λεπτά}}', + 'in {delta, plural, =1{a month} other{# months}}' => 'σε {delta, plural, =1{ένα μήνα} other{# μήνες}}', + 'in {delta, plural, =1{a second} other{# seconds}}' => 'σε {delta, plural, =1{ένα δευτερόλεπτο} other{# δευτερόλεπτα}}', + 'in {delta, plural, =1{a year} other{# years}}' => 'σε {delta, plural, =1{ένα έτος} other{# έτη}}', + 'in {delta, plural, =1{an hour} other{# hours}}' => 'σε {delta, plural, =1{μία ώρα} other{# ώρες}}', + 'just now' => 'μόλις τώρα', 'the input value' => 'η τιμή εισόδου', '{attribute} "{value}" has already been taken.' => 'Το {attribute} «{value}» έχει ήδη καταχωρηθεί.', '{attribute} cannot be blank.' => 'Το «{attribute}» δεν μπορεί να είναι κενό.', @@ -114,4 +83,32 @@ return [ '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => 'Το «{attribute}» πρέπει να περιέχει το λιγότερο {min, number} {min, plural, one{χαρακτήρα} few{χαρακτήρες} many{χαρακτήρες} other{χαρακτήρες}}.', '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => 'Το «{attribute}» πρέπει να περιέχει το πολύ {max, number} {max, plural, one{χαρακτήρα} few{χαρακτήρες} many{χαρακτήρες} other{χαρακτήρες}}.', '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => 'Το «{attribute}» πρέπει να περιέχει {length, number} {length, plural, one{χαρακτήρα} few{χαρακτήρες} many{χαρακτήρες} other{χαρακτήρες}}.', + '{delta, plural, =1{a day} other{# days}} ago' => 'πριν {delta, plural, =1{μία ημέρα} other{# ημέρες}}', + '{delta, plural, =1{a minute} other{# minutes}} ago' => 'πριν {delta, plural, =1{ένα λεπτό} other{# λεπτά}}', + '{delta, plural, =1{a month} other{# months}} ago' => 'πριν {delta, plural, =1{ένα μήνα} other{# μήνες}}', + '{delta, plural, =1{a second} other{# seconds}} ago' => 'πριν {delta, plural, =1{ένα δευτερόλεπτο} other{# δευτερόλεπτα}}', + '{delta, plural, =1{a year} other{# years}} ago' => 'πριν {delta, plural, =1{ένα έτος} other{# έτη}}', + '{delta, plural, =1{an hour} other{# hours}} ago' => 'πριν {delta, plural, =1{μία ώρα} other{# ώρες}}', + '{nFormatted} B' => '{nFormatted} B', + '{nFormatted} GB' => '{nFormatted} GB', + '{nFormatted} GiB' => '{nFormatted} GiB', + '{nFormatted} KB' => '{nFormatted} KB', + '{nFormatted} KiB' => '{nFormatted} KiB', + '{nFormatted} MB' => '{nFormatted} MB', + '{nFormatted} MiB' => '{nFormatted} MiB', + '{nFormatted} PB' => '{nFormatted} PB', + '{nFormatted} PiB' => '{nFormatted} PiB', + '{nFormatted} TB' => '{nFormatted} TB', + '{nFormatted} TiB' => '{nFormatted} TiB', + '{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} {n, plural, =1{byte} other{bytes}}', + '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', + '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', + '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}' => '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', + '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}' => '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', + '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}' => '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', + '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}' => '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', + '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}' => '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', + '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', + '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', + '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', ]; diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index a67ac93..61f417c 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -64,11 +64,11 @@ class AssetManager extends Component */ public $bundles = []; /** - * @return string the root directory storing the published asset files. + * @var string the root directory storing the published asset files. */ public $basePath = '@webroot/assets'; /** - * @return string the base URL through which the published asset files can be accessed. + * @var string the base URL through which the published asset files can be accessed. */ public $baseUrl = '@web/assets'; /** diff --git a/framework/web/Response.php b/framework/web/Response.php index 675a997..bb5ec8a 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -934,7 +934,9 @@ class Response extends \yii\base\Response throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface."); } } elseif ($this->format === self::FORMAT_RAW) { - $this->content = $this->data; + if ($this->data !== null) { + $this->content = $this->data; + } } else { throw new InvalidConfigException("Unsupported response format: {$this->format}"); } diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index 23f06dd..860b655 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -550,24 +550,9 @@ class ActiveField extends Component * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, + * @param array $options the tag options in terms of name-value pairs. * - * ~~~ - * [ - * 'value1' => ['disabled' => true], - * 'value2' => ['label' => 'value 2'], - * ]; - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * For the list of available options please refer to the `$options` parameter of [[\yii\helpers\Html::activeDropDownList()]]. * * If you set a custom `id` for the input element, you may need to adjust the [[$selectors]] accordingly. * @@ -593,28 +578,9 @@ class ActiveField extends Component * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * [ - * 'value1' => ['disabled' => true], - * 'value2' => ['label' => 'value 2'], - * ]; - * ~~~ + * @param array $options the tag options in terms of name-value pairs. * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - unselect: string, the value that will be submitted when no option is selected. - * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple - * mode, we can still obtain the posted unselect value. If you do not want any hidden input, - * you should explicitly set this option as null. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * For the list of available options please refer to the `$options` parameter of [[\yii\helpers\Html::activeListBox()]]. * * If you set a custom `id` for the input element, you may need to adjust the [[$selectors]] accordingly. * @@ -636,23 +602,8 @@ class ActiveField extends Component * The selection of the checkbox list is taken from the value of the model attribute. * @param array $items the data item used to generate the checkboxes. * The array values are the labels, while the array keys are the corresponding checkbox values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the checkbox list. The following options are specially handled: - * - * - unselect: string, the value that should be submitted when none of the checkboxes is selected. - * By setting this option, a hidden input will be generated. If you do not want any hidden input, - * you should explicitly set this option as null. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the checkbox in the whole list; $label - * is the label for the checkbox; and $name, $value and $checked represent the name, - * value and the checked status of the checkbox input. + * @param array $options options (name => config) for the checkbox list. + * For the list of available options please refer to the `$options` parameter of [[\yii\helpers\Html::activeCheckboxList()]]. * @return static the field object itself */ public function checkboxList($items, $options = []) @@ -669,23 +620,8 @@ class ActiveField extends Component * The selection of the radio buttons is taken from the value of the model attribute. * @param array $items the data item used to generate the radio buttons. * The array values are the labels, while the array keys are the corresponding radio values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the radio button list. The following options are specially handled: - * - * - unselect: string, the value that should be submitted when none of the radio buttons is selected. - * By setting this option, a hidden input will be generated. If you do not want any hidden input, - * you should explicitly set this option as null. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the radio button in the whole list; $label - * is the label for the radio button; and $name, $value and $checked represent the name, - * value and the checked status of the radio button input. + * @param array $options options (name => config) for the radio button list. + * For the list of available options please refer to the `$options` parameter of [[\yii\helpers\Html::activeRadioList()]]. * @return static the field object itself */ public function radioList($items, $options = []) diff --git a/tests/unit/data/postgres.sql b/tests/unit/data/postgres.sql index 7aa7b9b..b3f19c8 100644 --- a/tests/unit/data/postgres.sql +++ b/tests/unit/data/postgres.sql @@ -125,7 +125,7 @@ CREATE TABLE "bool_values" ( ); -CREATE TABLE `animal` ( +CREATE TABLE "animal" ( id serial primary key, type varchar(255) not null ); diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php index bed726f..b305aaf 100644 --- a/tests/unit/framework/helpers/StringHelperTest.php +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -227,4 +227,13 @@ class StringHelperTest extends TestCase $this->assertTrue(StringHelper::endsWith('string', 'nG', false)); $this->assertTrue(StringHelper::endsWith('BüЯйΨ', 'ÜяЙΨ', false)); } + + public function testExplode() + { + $this->assertEquals(['It', 'is', 'a first', 'test'], StringHelper::explode("It, is, a first, test")); + $this->assertEquals(['It', 'is', 'a second', 'test'], StringHelper::explode("It+ is+ a second+ test", '+')); + $this->assertEquals(['Save', '', '', 'empty trimmed string'], StringHelper::explode("Save, ,, empty trimmed string", ',')); + $this->assertEquals(['Здесь', 'multibyte', 'строка'], StringHelper::explode("Здесь我 multibyte我 строка", '我')); + $this->assertEquals(['Disable', ' trim ', 'here but ignore empty'], StringHelper::explode("Disable, trim ,,,here but ignore empty", ',', false, true)); + } } diff --git a/tests/unit/framework/web/ResponseTest.php b/tests/unit/framework/web/ResponseTest.php index f0f8bda..8a9c22d 100644 --- a/tests/unit/framework/web/ResponseTest.php +++ b/tests/unit/framework/web/ResponseTest.php @@ -82,4 +82,23 @@ class ResponseTest extends \yiiunit\TestCase { return '12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'; } + + /** + * https://github.com/yiisoft/yii2/issues/7529 + */ + public function testSendContentAsFile() + { + ob_start(); + $this->response->sendContentAsFile('test', 'test.txt')->send([ + 'mimeType' => 'text/plain' + ]); + $content = ob_get_clean(); + + static::assertEquals('test', $content); + static::assertEquals(200, $this->response->statusCode); + $headers = $this->response->headers; + static::assertEquals('application/octet-stream', $headers->get('Content-Type')); + static::assertEquals('attachment; filename="test.txt"', $headers->get('Content-Disposition')); + static::assertEquals(4, $headers->get('Content-Length')); + } }