Commit 6c88183b by Qiang Xue

Merge commit 'd3ff343e' into feature-restapi

Conflicts: framework/helpers/BaseArrayHelper.php
parents 9beba31f d3ff343e
service_name: travis-ci
src_dir: framework/yii
coverage_clover: tests/unit/runtime/coveralls/clover.xml
json_path: tests/unit/runtime/coveralls/coveralls-upload.json
\ No newline at end of file
imports:
- php
tools:
external_code_coverage:
timeout: 2100 # Timeout in seconds.
......@@ -15,7 +15,6 @@ services:
install:
- composer self-update && composer --version
# core framework:
# - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist
- composer install --prefer-dist
- tests/unit/data/travis/mongodb-setup.sh
- tests/unit/data/travis/apc-setup.sh
......@@ -35,9 +34,9 @@ before_script:
- mongo yii2test --eval 'db.addUser("travis", "test");'
script:
# - vendor/bin/phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,vendor
- vendor/bin/phpunit --verbose --exclude-group mssql,oci,wincache,xcache,zenddata
- vendor/bin/phpunit --verbose --coverage-clover=coverage.clover --exclude-group mssql,oci,wincache,xcache,zenddata
- cd apps/basic && php vendor/bin/codecept run
#after_script:
# - php vendor/bin/coveralls
after_script:
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover
......@@ -21,6 +21,7 @@ which is the latest stable release of Yii.
[![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2/v/stable.png)](https://packagist.org/packages/yiisoft/yii2)
[![Total Downloads](https://poser.pugx.org/yiisoft/yii2/downloads.png)](https://packagist.org/packages/yiisoft/yii2)
[![Build Status](https://secure.travis-ci.org/yiisoft/yii2.png)](http://travis-ci.org/yiisoft/yii2)
[![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/yii2/badges/coverage.png?s=31d80f1036099e9d6a3e4d7738f6b000b3c3d10e)](https://scrutinizer-ci.com/g/yiisoft/yii2/)
[![Dependency Status](https://www.versioneye.com/php/yiisoft:yii2/dev-master/badge.png)](https://www.versioneye.com/php/yiisoft:yii2/dev-master)
[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/yiisoft/yii2/badges/quality-score.png?s=b1074a1ff6d0b214d54fa5ab7abbb90fc092471d)](https://scrutinizer-ci.com/g/yiisoft/yii2/)
......@@ -49,8 +50,9 @@ DOCUMENTATION
A draft of the [Definitive Guide](docs/guide/index.md) is available.
API docs and a (quite bad) rendering of the definitive guide are currently
API docs and a rendering of the definitive guide are currently
available at http://stuff.cebe.cc/yii2docs/ (updated four times per hour).
We will make guide and API docs available on yiiframework.com with the beta release.
For 1.1 users, you may refer to [Upgrading from Yii 1.1](docs/guide/upgrade-from-v1.md)
to have a general idea of what has changed in 2.0.
......
......@@ -18,5 +18,6 @@ require_once(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
// set correct script paths
$_SERVER['SCRIPT_FILENAME'] = TEST_ENTRY_FILE;
$_SERVER['SCRIPT_NAME'] = TEST_ENTRY_URL;
$_SERVER['SERVER_NAME'] = 'localhost';
Yii::setAlias('@tests', __DIR__);
......@@ -10,4 +10,4 @@ return yii\helpers\ArrayHelper::merge(
],
],
]
);
\ No newline at end of file
);
......@@ -10,4 +10,4 @@ return yii\helpers\ArrayHelper::merge(
],
],
]
);
\ No newline at end of file
);
......@@ -10,4 +10,4 @@ return yii\helpers\ArrayHelper::merge(
],
],
]
);
\ No newline at end of file
);
......@@ -7,7 +7,6 @@ use yii\codeception\TestCase;
class ContactFormTest extends TestCase
{
use \Codeception\Specify;
protected function setUp()
......@@ -42,7 +41,7 @@ class ContactFormTest extends TestCase
expect('email file should exist', file_exists($this->getMessageFile()))->true();
});
$this->specify('message should contain correct data', function () use($model) {
$this->specify('message should contain correct data', function () use ($model) {
$emailMessage = file_get_contents($this->getMessageFile());
expect('email should contain user name', $emailMessage)->contains($model->name);
......@@ -56,5 +55,4 @@ class ContactFormTest extends TestCase
{
return Yii::getAlias(Yii::$app->mail->fileTransportPath) . '/testing_message.eml';
}
}
......@@ -8,7 +8,6 @@ use app\models\User;
class LoginFormTest extends TestCase
{
use \Codeception\Specify;
protected function tearDown()
......@@ -24,7 +23,7 @@ class LoginFormTest extends TestCase
$model->username = 'some_username';
$model->password = 'some_password';
$this->specify('user should not be able to login, when there is no identity' , function () use ($model) {
$this->specify('user should not be able to login, when there is no identity', function () use ($model) {
expect('model should not login user', $model->login())->false();
expect('user should not be logged in', Yii::$app->user->isGuest)->true();
});
......@@ -51,7 +50,7 @@ class LoginFormTest extends TestCase
$model->username = 'demo';
$model->password = 'demo';
$this->specify('user should be able to login with correct credentials', function() use ($model) {
$this->specify('user should be able to login with correct credentials', function () use ($model) {
expect('model should login user', $model->login())->true();
expect('error message should not be set', $model->errors)->hasntKey('password');
expect('user should be logged in', Yii::$app->user->isGuest)->false();
......@@ -60,9 +59,8 @@ class LoginFormTest extends TestCase
private function mockUser($user)
{
$loginForm = $this->getMock('app\models\LoginForm',['getUser']);
$loginForm = $this->getMock('app\models\LoginForm', ['getUser']);
$loginForm->expects($this->any())->method('getUser')->will($this->returnValue($user));
return $loginForm;
}
}
......@@ -40,7 +40,7 @@ AppAsset::register($this);
['label' => 'Contact', 'url' => ['/site/contact']],
Yii::$app->user->isGuest ?
['label' => 'Login', 'url' => ['/site/login']] :
['label' => 'Logout (' . Yii::$app->user->identity->username . ')' ,
['label' => 'Logout (' . Yii::$app->user->identity->username . ')',
'url' => ['/site/logout'],
'linkOptions' => ['data-method' => 'post']],
],
......
......@@ -716,7 +716,7 @@ Important points are:
Second, override [[yii\db\ActiveRecord::createQuery()]] to use the custom query class instead of the regular [[yii\db\ActiveQuery|ActiveQuery]].
For the example above, you need to write the following code:
```
```php
namespace app\models;
use yii\db\ActiveRecord;
......
......@@ -2,20 +2,25 @@ Managing assets
===============
An asset in Yii is a file that is included into the page. It could be CSS, JavaScript or
any other file. Framework provides many ways to work with assets from basics such as adding `<script src="...">` tag
for a file that is [handled by View](view.md) section to advanced usage such as publishing files that are not
under the webservers document root, resolving JavaScript dependencies or minifying CSS.
any other file. The framework provides many ways to work with assets from basics such as adding `<script src="...">` tags
for a file which is covered by the [View section](view.md), to advanced usage such as publishing files that are not
under the webservers document root, resolving JavaScript dependencies or minifying CSS, which we will cover in the following.
Declaring asset bundle
----------------------
In order to publish some assets you should declare an asset bundle first. The bundle defines a set of asset files or
directories to be published and their dependencies on other asset bundles.
Declaring asset bundles
-----------------------
Both basic and advanced application templates contain `AppAsset` asset bundle class that defines assets required
application wide. An asset bundle class always extends from [[yii\web\AssetBundle]].
In order to define a set of assets the belong together and should be used on the website you declare a class called
an "asset bundle". The bundle defines a set of asset files and their dependencies on other asset bundles.
Let's review basic application's asset bundle class:
Asset files can be located under the webservers accessable directory but also hidden inside of application or vendor
directories. If the latter, the asset bundle will care for publishing them to a directory accessible by the webserver
so they can be included in the website. This feature is useful for extensions so that they can ship all content in one
directory and make installation easier for you.
To define an asset you create a class extending from [[yii\web\AssetBundle]] and set the properties according to your needs.
Here you can see an example asset definition which is part of the basic application template, the
`AppAsset` asset bundle class. It defines assets required application wide:
```php
<?php
......@@ -39,20 +44,23 @@ class AppAsset extends AssetBundle
```
In the above `$basePath` specifies web-accessible directory assets are served from. It is a base for relative
`$css` and `$js` paths i.e. `@webroot/css/site.css` for `css/site.css`. Here `@webroot` is an alias that points to
`$css` and `$js` paths i.e. `@webroot/css/site.css` for `css/site.css`. Here `@webroot` is an [alias][] that points to
application's `web` directory.
`$baseUrl` is used to specify base URL for the same relative `$css` and `$js` i.e. `@web/css/site.css` where `@web`
is an alias that corresponds to your website base URL such as `http://example.com/`.
is an [alias][] that corresponds to your website base URL such as `http://example.com/`.
In case you have asset files under non web accessible directory, that is the case for any extension, you need
to additionally specify `$sourcePath`. Files will be copied or symlinked from source path to base path prior to being
registered. In case source path is used `baseUrl` is generated automatically at the time of publishing asset bundle.
In case you have asset files under a non web accessible directory, that is the case for any extension, you need
to specify `$sourcePath` instead of `$basePath` and `$baseUrl`. Files will be copied or symlinked from source path
to the `web/assets` directory of your application prior to being registered.
In this case `$basePath` and `$baseUrl` are generated automatically at the time of publishing the asset bundle.
Dependencies on other asset bundles are specified via `$depends` property. It is an array that contains fully qualified
names of bundle classes that should be published in order for this bundle to work properly.
class names of bundle classes that should be published in order for this bundle to work properly.
Javascript and CSS files for `AppAsset` are added to the header after the files of [[yii\web\YiiAsset]] and
[[yii\bootstrap\BootstrapAsset]] in this example.
Here `yii\web\YiiAsset` adds Yii's JavaScript library while `yii\bootstrap\BootstrapAsset` includes
Here [[yii\web\YiiAsset]] adds Yii's JavaScript library while [[yii\bootstrap\BootstrapAsset]] includes
[Bootstrap](http://getbootstrap.com) frontend framework.
Asset bundles are regular classes so if you need to define another one, just create alike class with unique name. This
......@@ -62,6 +70,9 @@ Additionally you may specify `$jsOptions`, `$cssOptions` and `$publishOptions` t
[[yii\web\View::registerJsFile()]], [[yii\web\View::registerCssFile()]] and [[yii\web\AssetManager::publish()]]
respectively during registering and publising an asset.
[alias]: basics.md#path-aliases "Yii Path alias"
### Language-specific asset bundle
If you need to define an asset bundle that includes JavaScript file depending on the language you can do it the
......@@ -82,11 +93,14 @@ class LanguageAsset extends AssetBundle
}
```
Registering asset bundle
------------------------
Asset bundle classes are typically registered in views or, if it's main application asset, in layout. Doing it is
as simple as:
Asset bundle classes are typically registered in view files or [widgets](view.md#widgets) that depend on the css or
javascript files for providing its functionality. An exception to this is the `AppAsset` class defined above which is
added in the applications main layout file to be registered on any page of the application.
Registering an asset bundle is as simple as calling the [[yii\web\AssetBundle::register()|register()]] method:
```php
use app\assets\AppAsset;
......@@ -94,6 +108,12 @@ AppAsset::register($this);
```
Since we're in a view context `$this` refers to `View` class.
To register an asset inside of a widget, the view instance is available as `$this->view`:
```php
AppAsset::register($this->view);
```
Overriding asset bundles
------------------------
......@@ -118,13 +138,14 @@ return [
];
```
In the above we're adding asset bundle definitions to `bundles` property of asset manager. Keys there are fully
qualified class paths to asset bundle classes we want to override while values are key-value arrays of class properties
In the above we're adding asset bundle definitions to the [[yii\web\AssetManager::bundles|bundles]] property of asset manager. Keys are fully
qualified class names to asset bundle classes we want to override while values are key-value arrays of class properties
and corresponding values to set.
Setting `sourcePath` to `null` tells asset manager not to copy anything while `js` overrides local files with a link
to CDN.
Enabling symlinks
-----------------
......@@ -146,6 +167,7 @@ return [
There are two main benefits in enabling it. First it is faster since no copying is required and second is that assets
will always be up to date with source files.
Compressing and combining assets
--------------------------------
......
......@@ -132,7 +132,7 @@ return [
'manageThing0' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
'manageThing1' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
'manageThing2' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
'manageThing2' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
'manageThing3' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
// AND THE ROLES
'guest' => [
......
......@@ -9,9 +9,13 @@ code execution. Unlike [PHP's traits](http://www.php.net/traits), behaviors can
Using behaviors
---------------
A behavior can be attached to any class that extends from [[yii\base\Component]]. In order to attach a behavior to a class,
the component class must implement the `behaviors`
method. As an example, Yii provides the [[yii\behaviors\TimestampBehavior]] behavior for automatically updating timestamp
A behavior can be attached to any class that extends from [[yii\base\Component]] either from code or via application
config.
### Attaching behaviors via `behaviors` method
In order to attach a behavior to a class you can implement the `behaviors` method of the component.
As an example, Yii provides the [[yii\behaviors\TimestampBehavior]] behavior for automatically updating timestamp
fields when saving an [[yii\db\ActiveRecord|Active Record]] model:
```php
......@@ -69,6 +73,38 @@ class User extends ActiveRecord
}
```
### Attaching behaviors dynamically
Another way to attach a behavior to a component is calling `attachBehavior` method like the followig:
```php
$component = new MyComponent();
$component->attachBehavior();
```
### Attaching behaviors from config
One can attach a behavior to a component when configuring it with a configuration array. The syntax is like the
following:
```php
return [
// ...
'components' => [
'myComponent' => [
// ...
'as tree' => [
'class' => 'Tree',
'root' => 0,
],
],
],
];
```
In the config above `as tree` stands for attaching a behavior named `tree`, and the array will be passed to [[\Yii::createObject()]]
to create the behavior object.
Creating your own behaviors
---------------------------
......
......@@ -244,7 +244,24 @@ Two other filters, [[yii\web\PageCache]] and [[yii\web\HttpCache]] are described
Catching all incoming requests
------------------------------
TBD
Sometimes it is useful to handle all incoming requests with a single controller action. For example, displaying a notice
when website is in maintenance mode. In order to do it you should configure web application `catchAll` property either
dynamically or via application config:
```php
$config = [
'id' => 'basic',
'basePath' => dirname(__DIR__),
// ...
'catchAll' => [ // <-- here
'offline/notice',
'param1' => 'value1',
'param2' => 'value2',
],
```
In the above `offline/notice` refer to `OfflineController::actionNotice()`. `param1` and `param2` are parameters passed
to action method.
Custom response class
---------------------
......
Data grid
=========
Data grid or GridView is one of the most powerful Yii widgets. It is extremely useful if you need to quickly build admin
section of the system. It takes data from [data provider](data-provider.md) and renders each row using a set of columns
presenting data in a form of a table.
Each row of the table represents the data of a single data item, and a column usually represents an attribute of
the item (some columns may correspond to complex expression of attributes or static text).
Grid view supports both sorting and pagination of the data items. The sorting and pagination can be done in AJAX mode
or normal page request. A benefit of using GridView is that when the user disables JavaScript, the sorting and pagination
automatically degrade to normal page requests and are still functioning as expected.
The minimal code needed to use CGridView is as follows:
```php
sdf
```
The above code first creates a data provider and then uses GridView to display every attribute in every row taken from
data provider. The displayed table is equiped with sorting and pagination functionality.
Grid columns
------------
Yii grid consists of a number of columns. Depending on column type and settings these are able to present data differently.
These are defined in the columns part of GridView config like the following:
```php
echo GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
// A simple column defined by the data contained in $dataProvider.
// Data from model's column1 will be used.
'id',
'username',
// More complex one.
[
'class' => 'DataColumn', // can be omitted, default
'name' => 'column1',
'value' => function ($data) {
return $data->name;
},
'type'=>'raw',
],
],
]);
```
Note: If columns part of config isn't specified, Yii tries to show all possible data provider model columns.
### Column classes
#### Data column
#### Action column
#### Checkbox column
#### Serial column
TODO: rewrite these:
- https://github.com/samdark/a-guide-to-yii-grids-lists-and-data-providers/blob/master/grid-columns.md
- https://github.com/samdark/a-guide-to-yii-grids-lists-and-data-providers/pull/1
Sorting data
------------
- https://github.com/yiisoft/yii2/issues/1576
Filtering data
--------------
- https://github.com/yiisoft/yii2/issues/1581
\ No newline at end of file
Data and widgets for it
=======================
One of the most powerful aspects of Yii is how it works with data. One may output data directly and that's a good approach
for website frontend but when it comes to backend data components and widgets may save you weeks.
Typically, you would take the following steps when working with one of these data components:
1. Configure data provider. It may take its data from array, SQL, AR query etc.
2. Pass data provider to one of the widgets such as list view or grid view.
3. Customize the widget to reflect the presentational style that you are after.
That's it. After doing these simple steps you can get a full featured data grid supporting pagination, sorting and
filtering that is ideal for admin part of your project.
\ No newline at end of file
Data providers
==============
Data provider abstracts data set via [[yii\data\DataProviderInterface]] and handles pagination and sorting.
It can be used by [grids](data-grid.md), [lists and other data widgets](data-widgets.md).
In Yii there are three built-in data providers: [[yii\data\ActiveDataProvider]], [[yii\data\ArrayDataProvider]] and
[[yii\data\SqlDataProvider]].
Active data provider
--------------------
`ActiveDataProvider` provides data by performing DB queries using [[\yii\db\Query]] and [[\yii\db\ActiveQuery]].
The following is an example of using it to provide ActiveRecord instances:
```php
$provider = new ActiveDataProvider([
'query' => Post::find(),
'pagination' => [
'pageSize' => 20,
],
]);
// get the posts in the current page
$posts = $provider->getModels();
~~~
And the following example shows how to use ActiveDataProvider without ActiveRecord:
```php
$query = new Query;
$provider = new ActiveDataProvider([
'query' => $query->from('tbl_post'),
'pagination' => [
'pageSize' => 20,
],
]);
// get the posts in the current page
$posts = $provider->getModels();
```
Array data provider
-------------------
ArrayDataProvider implements a data provider based on a data array.
The [[allModels]] property contains all data models that may be sorted and/or paginated.
ArrayDataProvider will provide the data after sorting and/or pagination.
You may configure the [[sort]] and [[pagination]] properties to
customize the sorting and pagination behaviors.
Elements in the [[allModels]] array may be either objects (e.g. model objects)
or associative arrays (e.g. query results of DAO).
Make sure to set the [[key]] property to the name of the field that uniquely
identifies a data record or false if you do not have such a field.
Compared to `ActiveDataProvider`, `ArrayDataProvider` could be less efficient
because it needs to have [[allModels]] ready.
ArrayDataProvider may be used in the following way:
```php
$query = new Query;
$provider = new ArrayDataProvider([
'allModels' => $query->from('tbl_post')->all(),
'sort' => [
'attributes' => ['id', 'username', 'email'],
],
'pagination' => [
'pageSize' => 10,
],
]);
// get the posts in the current page
$posts = $provider->getModels();
```
> Note: if you want to use the sorting feature, you must configure the [[sort]] property
so that the provider knows which columns can be sorted.
SQL data provider
-----------------
SqlDataProvider implements a data provider based on a plain SQL statement. It provides data in terms of arrays, each
representing a row of query result.
Like other data providers, SqlDataProvider also supports sorting and pagination. It does so by modifying the given
[[sql]] statement with "ORDER BY" and "LIMIT" clauses. You may configure the [[sort]] and [[pagination]] properties to
customize sorting and pagination behaviors.
`SqlDataProvider` may be used in the following way:
```php
$count = Yii::$app->db->createCommand('
SELECT COUNT(*) FROM tbl_user WHERE status=:status
', [':status' => 1])->queryScalar();
$dataProvider = new SqlDataProvider([
'sql' => 'SELECTFROM tbl_user WHERE status=:status',
'params' => [':status' => 1],
'totalCount' => $count,
'sort' => [
'attributes' => [
'age',
'name' => [
'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
'default' => SORT_DESC,
'label' => 'Name',
],
],
],
'pagination' => [
'pageSize' => 20,
],
]);
// get the user records in the current page
$models = $dataProvider->getModels();
```
> Note: if you want to use the pagination feature, you must configure the [[totalCount]] property
to be the total number of rows (without pagination). And if you want to use the sorting feature,
you must configure the [[sort]] property so that the provider knows which columns can be sorted.
Implementing your own custom data provider
------------------------------------------
Data widgets
============
ListView
--------
DetailView
----------
DetailView displays the detail of a single data [[model]].
It is best used for displaying a model in a regular format (e.g. each model attribute is displayed as a row in a table).
The model can be either an instance of [[Model]] or an associative array.
DetailView uses the [[attributes]] property to determines which model attributes should be displayed and how they
should be formatted.
A typical usage of DetailView is as follows:
```php
echo DetailView::widget([
'model' => $model,
'attributes' => [
'title', // title attribute (in plain text)
'description:html', // description attribute in HTML
[ // the owner name of the model
'label' => 'Owner',
'value' => $model->owner->name,
],
],
]);
```
Events
======
TBD, see also [Component.md](../api/base/Component.md).
Event is a way to "inject" custom code into existing code at certain places. For example, a comment object can trigger
an "add" event when the user adds a comment. We can write custom code and attach it to this event so that when the event
is triggered (i.e. comment will be added), our custom code will be executed.
[[ADD INTRODUCTION]]
Events are very useful both to make your components flexible and to hook into framework and extensions workflow.
Creating Event Handlers
-----------------------
Triggering events
-----------------
In Yii 1, events were defined using the `onEventName` method syntax, such as `onBeforeSave`. This is no longer necessary in Yii 2, as event handling is now assigned using the `on` method. The method's first argument is the name of the event to watch for; the second is the handling method to be called when that event occurs:
Any component can trigger events using `trigger` method:
```php
$component->on($eventName, $handler);
$this->trigger('myEvent');
// or
$event = new CreateUserEvent(); // extended from yii\base\Event
$event->userName = 'Alexander';
$this->trigger('createUserEvent', $event);
```
[[LINK TO LIST OF EVENTS]]
Event name should be unique within the class it is defined at. Event names are *case-sensitive*. It is a good practice
to define event names using class constants:
```php
class Mailer extends Component
{
const EVENT_SEND_EMAIL = 'sendEmail';
public function send()
{
// ...
$this->trigger(self::EVENT_SEND_EMAIL);
}
}
```
Attaching event handlers
------------------------
One or multiple PHP callbacks, called *event handlers*, can be attached to an event. When an event is raised, the event
handlers will be invoked automatically in the order they were attached.
There are two main methods of attaching event handlers. It can be done either via code or via application config.
> Tip: In order to get up to date list of framework and extension events search code for `->trigger`.
### Attaching event handlers via code
You can assign event handlers using `on` method of the component instance. The method's first argument is the name of
the event to watch for; the second is the handler to be called when that event occurs:
```php
$component->on($eventName, $handler);
```
The handler must be a valid PHP callback. This could be represented as:
* The name of a global function
* An array consisting of a model name and method name
* An array consisting of an object and a method name
* An anonymous function
- The name of a global function.
- An array consisting of a model name and method name.
- An array consisting of an object and a method name.
- An anonymous function.
```php
// Global function:
......@@ -34,7 +75,7 @@ $component->on($eventName, ['Modelname', 'functionName']);
$component->on($eventName, [$obj, 'functionName']);
// Anonymous function:
$component->on($eventName, function($event) {
$component->on($eventName, function ($event) {
// Use $event.
});
```
......@@ -42,6 +83,31 @@ $component->on($eventName, function($event) {
As shown in the anonymous function example, the event handling function must be defined so that it takes one argument.
This will be an [[yii\base\Event]] object.
In order to pass extra data supply it via third argument:
```php
$component->on($eventName, function ($event) {
// the extra data can be accessed via $event->data
}, $extraData);
```
### Attaching event handlers via config
It is possible to use application config to attach event hanelers:
```php
return [
// ...
'components' => [
'db' => [
// ...
'on afterOpen' => function ($event) {
// do something right after connected to database
}
],
],
];
```
Removing Event Handlers
-----------------------
......@@ -52,7 +118,8 @@ The correspondoing `off` method removes an event handler:
// $component->off($eventName);
```
Yii supports the ability to associate multiple handlers with the same event. When using `off` as in the above, every handler is removed. To remove only a specific handler, provide that as the second argument to `off`:
Yii supports the ability to associate multiple handlers with the same event. When using `off` as in the above,
every handler is removed. To remove only a specific handler, provide that as the second argument to `off`:
```php
// $component->off($eventName, $handler);
......@@ -60,39 +127,27 @@ Yii supports the ability to associate multiple handlers with the same event. Whe
The `$handler` should be presented in the `off` method in the same way as was presented in `on` in order to remove it.
Event Parameters
----------------
You can make your event handlers easier to work with and more powerful by passing additional values as parameters.
```php
$component->on($eventName, $handler, $params);
```
The passed parameters will be available in the event handler through `$event->data`, which will be an array.
[[NEED TO CONFIRM THE ABOVE]]
Global Events
-------------
Thanks to the change in Yii 2 as to how event handlers are created, you can now use "global" events. To create a global event, simply attach handlers to an event on the application instance:
You can use "global" events instead of per-component ones. To trigger a global event use an application instance instead
of specific component:
```php
Yii::$app->on($eventName, $handler);
Yii::$app->trigger($eventName);
```
You can use the `trigger` method to trigger these events manually:
In order to attach a handler to it use the following:
```php
// this will trigger the event and cause $handler to be invoked:
Yii::$app->trigger($eventName);
Yii::$app->on($eventName, $handler);
```
Class Events
------------
You can also attach event handlers to all instances of a class instead of individual instances. To do so, use the static `Event::on` method:
It is possible to attach event handlers to all instances of a class instead of individual instances. To do so, use
the static `Event::on` method:
```php
Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
......
Extending Yii
=============
The Yii framework was designed to be easily extendable. Additional features can be added to your project and then reused, either by yourself on other projects or by sharing your work as a formal Yii extension.
Code style
----------
- Use [core framework code style](https://github.com/yiisoft/yii2/wiki/Core-framework-code-style).
- Document classes, methods and properties using phpdoc. Note that you can use markdown and link to properties and methods
using the following syntax: e.g. `[[name()]]`, `[[name\space\MyClass::name()]]`.
- Extension classes should not be prefixed. No `TbNavBar`, `EMyWidget`, etc.
To be consistent with core Yii conventions, your extensions ought to adhere to certain coding styles:
- Use the [core framework code style](https://github.com/yiisoft/yii2/wiki/Core-framework-code-style).
- Document classes, methods and properties using [phpdoc](http://www.phpdoc.org/). - Extension classes should *not* be prefixed. Do not use the format `TbNavBar`, `EMyWidget`, etc.
> Note that you can use Markdown within your code for documentation purposes. With Markdown, you can link to properties and methods using the following syntax: `[[name()]]`, `[[namespace\MyClass::name()]]`.
### Namespace
- Do not use `yiisoft` in the namespaces.
- Do not use `\yii`, `\yii2` or `\yiisoft` as root namespace.
- Namespace should be `vendorName\uniqueName`.
Yii 2 relies upon namespaces to organize code. (Namespace support was added to PHP in version 5.3.) If you want to use namespaces within your extension,
- Do not use `yiisoft` anywhere in your namespaces.
- Do not use `\yii`, `\yii2` or `\yiisoft` as root namespaces.
- Namespaces should use the syntax `vendorName\uniqueName`.
Choosing unique namespace is important to prevent name collisions and autoload faster. Examples:
Choosing a unique namespace is important to prevent name collisions, and also results in faster autoloading of classes. Examples of unique, consistent namepacing are:
- `samdark\wiki`
- `samdark\debugger`
......@@ -24,15 +29,22 @@ Choosing unique namespace is important to prevent name collisions and autoload f
Distribution
------------
- There should be a `readme.md` file clearly describing what extension does in English, its requirements, how to install
and use it. It should be written using markdown. If you want to provide translated readme, name it as `readme_ru.md`
where `ru` is your language code. If extension provides a widget it is a good idea to include some screenshots.
- It is recommended to host your extensions at [Github](github.com).
- Extension should be registered at [Packagist](https://packagist.org) in order to be installable via Composer.
Choose package name wisely since changing it leads to losing stats and inability to install package by the old name.
Beyond the code itself, the entire extension distribution ought to have certain things.
There should be a `readme.md` file, written in English. This file should clearly describe what the extension does, its requirements, how to install it,
and to use it. The README should be written using Markdown. If you want to provide translated README files, name them as `readme_ru.md`
where `ru` is your language code (in this case, Russian).
It is a good idea to include some screenshots as part of the documentation, especially if your extension provides a widget.
It is recommended to host your extensions at [Github](github.com).
Extensions should also be registered at [Packagist](https://packagist.org) in order to be installable via Composer.
### Composer package name
Choose your extension's package name wisely, as you shouldn't change the package name later on. (Changing the name leads to losing the Composer stats, and makes it impossible for people to install the package by the old name.)
If your extension was made specifically for Yii2 (i.e. cannot be used as a standalone PHP library) it is recommended to
name it like the following:
......@@ -40,32 +52,35 @@ name it like the following:
yii2-my-extension-name-type
```
In the above:
Where:
- `yii2-` prefix.
- Extension name lowecase, words separated by `-`.
- `-type` postfix where type may be `widget`, `behavior`, `module` etc.
- `yii2-` is a prefix.
- The extension name is in all lowercase letters, with words separated by `-`.
- The `-type` postfix may be `widget`, `behavior`, `module` etc.
### Dependencies
- Additional code, eg. libraries, should be required in your `composer.json` file.
- When extension is released in a stable version, its requirements should not include `dev` packages that do not
have a `stable` release.
- Use appropriate version constraints, eg. `1.*`, `@stable` for requirements.
Some extensions you develop may have their own dependencies, such as relying upon other extensions or third-party libraries. When dependencies exist, you should require them in your extension's `composer.json` file. Be certain to also use appropriate version constraints, eg. `1.*`, `@stable` for requirements.
Finally, when your extension is released in a stable version, double-check that its requirements do not include `dev` packages that do not have a `stable` release. In other words, the stable release of your extension should only rely upon stable dependencies.
### Versioning
As you maintain and upgrading your extension,
- Use the rules of [semantic versioning](http://semver.org).
- Use a consistent format for your repository tags, as they are treated as version strings by composer, eg. `0.2.4`,
`0.2.5`,`0.3.0`,`1.0.0`.
### composer.json
Yii2 uses Composer for installation, and extensions for Yii2 should as well. Towards that end,
- Use the type `yii2-extension` in `composer.json` file if your extension is Yii-specific.
- Do not use `yii` or `yii2` as composer vendor name.
- Do not use `yiisoft` in the composer package name or the composer vendor name.
- Do not use `yii` or `yii2` as the Composer vendor name.
- Do not use `yiisoft` in the Composer package name or the Composer vendor name.
If your extension classes reside directly in repository root use PSR-4 the following way in your `composer.json`:
If your extension classes reside directly in the repository root directory, you can use the PSR-4 autoloader in the following way in your `composer.json` file:
```
{
......@@ -92,19 +107,21 @@ If your extension classes reside directly in repository root use PSR-4 the follo
}
```
In the above `myname/mywidget` is the package name that will be registered
at [Packagist](https://packagist.org). It is common for it to match your github repository.
In the above, `myname/mywidget` is the package name that will be registered
at [Packagist](https://packagist.org). It is common for the package name to match your Github repository name.
We're using `psr-4` autoloader and mapping `myname\mywidget` namespace to the root directory where our classes reside.
In the above, the `psr-4` autoloader is specified, mapping the `myname\mywidget` namespace to the root directory where the classes reside.
More details can be found in the [composer documentation](http://getcomposer.org/doc/04-schema.md#autoload).
More details on this syntax can be found in the [Composer documentation](http://getcomposer.org/doc/04-schema.md#autoload).
Working with database
---------------------
- If extension creates or modifies database schema always use Yii migrations instead of SQL files or custom scripts.
- Migrations should be appliable to as many data storages as possible.
- Do not use active record models in your migrations.
Extensions sometimes have to use their own database tables. In such situations,
- If the extension creates or modifies the database schema, always use Yii migrations instead of SQL files or custom scripts.
- Migrations should be applicable to as many data storages as possible.
- Do not use Active Record models in your migrations.
Assets
------
......
......@@ -45,8 +45,10 @@ If there's no translation for `ru-RU` Yii will try `ru` as well before failing.
> **Note**: you can further customize details specifying language
> [as documented in ICU project](http://userguide.icu-project.org/locale#TOC-The-Locale-Concept).
Basic message translation
-------------------------
Message translation
-------------------
### Basics
Yii basic message translation in its basic variant works without additional PHP extension. What it does is finding a
translation of the message from source language into target language. Message itself is specified as the second
......@@ -97,7 +99,7 @@ language is used.
Instead of configuring `fileMap` you can rely on convention which is `messages/BasePath/LanguageID/CategoryName.php`.
### Named placeholders
#### Named placeholders
You can add parameters to a translation message that will be substituted with the corresponding value after translation.
The format for this is to use curly brackets around the parameter name as you can see in the following example:
......@@ -111,7 +113,7 @@ echo \Yii::t('app', 'Hello, {username}!', [
Note that the parameter assignment is without the brackets.
### Positional placeholders
#### Positional placeholders
```php
$sum = 42;
......@@ -121,8 +123,8 @@ echo \Yii::t('app', 'Balance: {0}', $sum);
> **Tip**: Try keep message strings meaningful and avoid using too many positional parameters. Remember that
> translator has source string only so it should be obvious about what will replace each placeholder.
Advanced placeholder formatting
-------------------------------
### Advanced placeholder formatting
In order to use advanced features you need to install and enable [intl](http://www.php.net/manual/en/intro.intl.php) PHP
extension. After installing and enabling it you will be able to use extended syntax for placeholders. Either short form
......@@ -132,7 +134,7 @@ that allows you to specify formatting style.
Full reference is [available at ICU website](http://icu-project.org/apiref/icu4c/classMessageFormat.html) but since it's
a bit cryptic we have our own reference below.
### Numbers
#### Numbers
```php
$sum = 42;
......@@ -155,7 +157,7 @@ echo \Yii::t('app', 'Balance: {0, number, ,000,000000}', $sum);
[Formatting reference](http://icu-project.org/apiref/icu4c/classicu_1_1DecimalFormat.html).
### Dates
#### Dates
```php
echo \Yii::t('app', 'Today is {0, date}', time());
......@@ -175,7 +177,7 @@ echo \Yii::t('app', 'Today is {0, date, YYYY-MM-dd}', time());
[Formatting reference](http://icu-project.org/apiref/icu4c/classicu_1_1SimpleDateFormat.html).
### Time
#### Time
```php
echo \Yii::t('app', 'It is {0, time}', time());
......@@ -196,13 +198,13 @@ echo \Yii::t('app', 'It is {0, date, HH:mm}', time());
[Formatting reference](http://icu-project.org/apiref/icu4c/classicu_1_1SimpleDateFormat.html).
### Spellout
#### Spellout
```php
echo \Yii::t('app', '{n,number} is spelled as {n, spellout}', ['n' => 42]);
```
### Ordinal
#### Ordinal
```php
echo \Yii::t('app', 'You are {n, ordinal} visitor here!', ['n' => 42]);
......@@ -210,7 +212,7 @@ echo \Yii::t('app', 'You are {n, ordinal} visitor here!', ['n' => 42]);
Will produce "You are 42nd visitor here!".
### Duration
#### Duration
```php
......@@ -219,7 +221,7 @@ echo \Yii::t('app', 'You are here for {n, duration} already!', ['n' => 47]);
Will produce "You are here for 47 sec. already!".
### Plurals
#### Plurals
Different languages have different ways to inflect plurals. Some rules are very complex so it's very handy that this
functionality is provided without the need to specify inflection rule. Instead it only requires your input of inflected
......@@ -251,7 +253,7 @@ Total {count, number} {count, plural, one{item} other{items}}.
To learn which inflection forms you should specify for your language you can referrer to
[rules reference at unicode.org](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html).
### Selections
#### Selections
You can select phrases based on keywords. The pattern in this case specifies how to map keywords to phrases and
provides a default phrase.
......@@ -268,26 +270,7 @@ Will produce "Snoopy is dog and it loves Yii!".
In the expression `female` and `male` are possible values. `other` handler values that do not match. Strings inside
brackets are sub-expressions so could be just a string or a string with more placeholders.
Views
-----
You can use i18n in your views to provide support for different languages. For example, if you have view `views/site/index.php` and
you want to create special case for russian language, you create `ru-RU` folder under the view path of current controller/widget and
put there file for russian language as follows `views/site/ru-RU/index.php`.
> **Note**: If language is specified as `en-US` and there are no corresponding views, Yii will try views under `en`
> before using original ones.
Formatters
----------
In order to use formatters you need to install and enable [intl](http://www.php.net/manual/en/intro.intl.php) PHP
extension.
Examples
--------
###Specifying default translation
### Specifying default translation
You can specify default translation that will be used as a fallback for categories that don't match any other translation.
This translation should be marked with `*`. In order to do it add the following to the config file (for the `yii2-basic` application it will be `web.php`):
......@@ -313,7 +296,7 @@ echo Yii::t('not_specified_category', 'message from unspecified category');
Message will be loaded from `@app/messages/<LanguageCode>/not_specified_category.php`.
###Translating module messages
### Translating module messages
If you want to translate messages for a module and avoid using a single translation file for all messages, you can make it like the following:
......@@ -359,7 +342,7 @@ class Module extends \yii\base\Module
In the example above we are using wildcard for matching and then filtering each category per needed file. Instead of using `fileMap` you can simply
use convention of category mapping to the same named file and use `Module::t('validation', 'your custom validation message')` or `Module::t('form', 'some form label')` directly.
###Translating widgets messages
### Translating widgets messages
Same rules can be applied for widgets too, for example:
......@@ -410,4 +393,36 @@ Instead of using `fileMap` you can simply use convention of category mapping to
> **Note**: For widgets you also can use i18n views, same rules as for controllers are applied to them too.
TBD: provided classes overview.
Views
-----
You can use i18n in your views to provide support for different languages. For example, if you have view `views/site/index.php` and
you want to create special case for russian language, you create `ru-RU` folder under the view path of current controller/widget and
put there file for russian language as follows `views/site/ru-RU/index.php`.
> **Note**: If language is specified as `en-US` and there are no corresponding views, Yii will try views under `en`
> before using original ones.
i18n formatter
--------------
i18n formatter component is the localized version of formatter that supports formatting of date, time and numbers based
on current locale. In order to use it you need to configure formatter application component as follows:
```php
return [
// ...
'components' => [
'formatter' => [
'class' => 'yii\i18n\Formatter',
],
],
];
```
After cofiguring the component can be accessed as `Yii::$app->formatter`.
Note that in order to use i18n formatter you need to install and enable
[intl](http://www.php.net/manual/en/intro.intl.php) PHP extension.
In order to learn about formatter methods refer to its API documentation: [[yii\i18n\Formatter]].
\ No newline at end of file
......@@ -72,10 +72,10 @@ Security and access control
Data providers, lists and grids
-------------------------------
- Overview
- Data providers
- Grids
- Lists
- [Overview](data-overview.md)
- [Data providers](data-providers.md)
- [Data widgets](data-widgets.md)
- [Grid](data-grid.md)
Advanced Topics
---------------
......
......@@ -3,8 +3,8 @@ Installation
There are two ways you can install the Yii framework:
* Installation via [Composer](http://getcomposer.org/) (recommended)
* Download an application template packed with all requirements including the Yii Framework
* Via [Composer](http://getcomposer.org/) (recommended)
* Download an application template containing all site requirements, including the Yii framework itself
Installing via Composer
......
......@@ -76,17 +76,32 @@ When the application ends or [[yii\log\Logger::flushInterval|flushInterval]] is
[[yii\log\Logger::flush()|flush()]] to send logged messages to different log targets, such as file, email, web.
Configuring context information
-------------------------------
TDB
Profiling
---------
TBD
Performance profiling is a special type of message logging that can be used to measure the time needed for the
specified code blocks to execute and find out what the performance bottleneck is.
To use it we need to identify which code blocks need to be profiled. Then we mark the beginning and the end of each code
block by inserting the following methods:
- [[Yii::beginProfile()]]
- [[Yii::endProfile()]]
```php
\Yii::beginProfile('myBenchmark');
...code block being profiled...
\Yii::endProfile('myBenchmark');
```
where `myBenchmark` uniquely identifies the code block.
Note, code blocks need to be nested properly such as
```php
\Yii::beginProfile('block1');
// some code to be profiled
\Yii::beginProfile('block2');
// some other code to be profiled
\Yii::endProfile('block2');
\Yii::endProfile('block1');
```
Profiling results [could be displayed in debugger](module-debug.md).
......@@ -331,7 +331,7 @@ For the code above mass assignment will be allowed stsrictly according to `scena
$user = User::find(42);
$data = ['password' => '123'];
$user->attributes = $data;
print_r($data);
print_r($user->attributes);
```
Will give you empty array because there's no default scenario defined in our `scenarios()`.
......@@ -345,7 +345,7 @@ $data = [
'hashcode' => 'test',
];
$user->attributes = $data;
print_r($data);
print_r($user->attributes);
```
Will give you the following:
......@@ -386,7 +386,7 @@ $data = [
'password' => '123',
];
$user->attributes = $data;
print_r($data);
print_r($user->attributes);
```
Will give you the following:
......
......@@ -4,6 +4,19 @@ Debug toolbar and debugger
Yii2 includes a handy toolbar to aid faster development and debugging as well as debugger. Toolbar displays information
about currently opened page while using debugger you can analyze data collected before.
Out of the box it allows you to:
- Quickly getting framework version, PHP version, response status, current controller and action, performance info and
more via toolbar.
- Browsing application and PHP configuration.
- Browsing request data, request and response headers, session data and environment.
- Viewing, searching, filtering logs.
- View profiling results.
- View database queries.
- View emails sent.
All these are available per request so you can browse past requests as well.
Installing and configuring
--------------------------
......@@ -41,10 +54,128 @@ If you are using `enableStrictParsing` URL manager option, add the following to
],
```
How to use it
-------------
### Extra config for logging and profiling
Logging and profiling are simple but very powerful tools that may help you to understand execution flow of both the
framework and the application. These are useful both for development and production environments.
While in production environment you should log only important enough messages manually as described in
[logging guide section](logging.md), in development environment it's especially useful to get execution trace.
In order to get trace messages that help you to understand what happens under the hood of the framework, you need to set
trace level in the config:
```php
return [
// ...
'components' => [
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0, // <-- here
```
By default it's automatically set to `3` if Yii is run in debug mode i.e. your `index.php` file contains the following:
```php
defined('YII_DEBUG') or define('YII_DEBUG', true);
```
> Note: Make sure to disable debug mode on production since it may have significan performance effect and expose sensible
information to end users.
Creating your own panels
------------------------
Both toolbar and debugger are highly configurable and customizable. You can create your own panels that could collect
and display extra data. Below we'll describe a process of creation of a simple custom panel that collects views rendered
during request, shows a number in the toolbar and allows you checking view names in debugger. Below we're assuming
basic application template.
First we need to implement panel class in `panels/ViewsPanel.php`:
```php
<?php
namespace app\panels;
use yii\base\Event;
use yii\base\View;
use yii\base\ViewEvent;
use yii\debug\Panel;
class ViewsPanel extends Panel
{
private $_viewFiles = [];
public function init()
{
parent::init();
Event::on(View::className(), View::EVENT_BEFORE_RENDER, function (ViewEvent $event) {
$this->_viewFiles[] = $event->viewFile;
});
}
/**
* @inheritdoc
*/
public function getName()
{
return 'Views';
}
/**
* @inheritdoc
*/
public function getSummary()
{
$url = $this->getUrl();
$count = count($this->data);
return "<div class=\"yii-debug-toolbar-block\"><a href=\"$url\">Views <span class=\"label\">$count</span></a></div>";
}
/**
* @inheritdoc
*/
public function getDetail()
{
return '<ol><li>' . implode('<li>', $this->data) . '</ol>';
}
/**
* @inheritdoc
*/
public function save()
{
return $this->_viewFiles;
}
}
```
The workflow for the code above is the following:
1. `init` is executed before running any controller action. Best place to attach handlers that will collect data.
2. `save` is called after controller action is executed. Data returned is stored in data file. If nothing returned panel
won't render.
3. Data from data file is loaded into `$this->data`. For toolbar it's always latest data, for debugger it may be selected
to be read from any previous data file.
4. Toolbar takes its contents from `getSummary`. There we're showing a number of view files rendered. Debugger uses
`getDetail` for the same purpose.
Now it's time to tell debugger to use our new panel. In `config/web.php` debug configuration is modified to be the
following:
```php
if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
$config['preload'][] = 'debug';
$config['modules']['debug'] = [
'class' => 'yii\debug\Module',
'panels' => [
'views' => ['class' => 'app\panels\ViewsPanel'],
],
];
// ...
```
That's it. Now we have another useful panel without writing much code.
......@@ -5,4 +5,4 @@ This tutorial is released under [the Terms of Yii Documentation](http://www.yiif
All Rights Reserved.
2014 (c) Yii Software LLC.
&copy; 2014 Yii Software LLC.
......@@ -240,7 +240,7 @@ the above car dealer website as an example, we may declare the following URL rul
// ...
['class' => 'app\components\CarUrlRule', 'connectionID' => 'db', ...],
['class' => 'app\components\CarUrlRule', 'connectionID' => 'db', /* ... */],
],
],
],
......
......@@ -12,6 +12,7 @@ use phpDocumentor\Reflection\DocBlock\Type\Collection;
use yii\apidoc\models\MethodDoc;
use yii\apidoc\models\TypeDoc;
use yii\apidoc\templates\BaseRenderer;
use yii\helpers\Inflector;
use yii\helpers\Markdown;
/**
......@@ -100,7 +101,7 @@ class ApiMarkdown extends GithubMarkdown
// TODO improve code highlighting
if (strncmp($code, '<?php', 5) === 0) {
$text = highlight_string(trim($code), true);
$text = @highlight_string(trim($code), true);
} else {
$text = highlight_string("<?php ".trim($code), true);
$text = str_replace('&lt;?php', '', $text);
......@@ -201,6 +202,18 @@ class ApiMarkdown extends GithubMarkdown
}
/**
* @inheritDocs
*/
protected function renderHeadline($block)
{
$content = $this->parseInline($block['content']);
$hash = Inflector::slug(strip_tags($content));
$hashLink = "<a href=\"#$hash\" name=\"$hash\">&para;</a>";
$tag = 'h' . $block['level'];
return "<$tag>$content $hashLink</$tag>";
}
/**
* Converts markdown into HTML
*
* @param string $content
......
......@@ -50,7 +50,6 @@ abstract class BaseRenderer extends Component
* @param array $files list of markdown files to render
* @param Context $context the api documentation context to render.
* @param Controller $controller the apidoc controller instance. Can be used to control output.
* @return
*/
public abstract function renderMarkdownFiles($controller);
......
......@@ -9,6 +9,17 @@ use yii\helpers\Html;
\yii\apidoc\templates\bootstrap\assets\AssetBundle::register($this);
// Navbar hides initial content when jumping to in-page anchor
// https://github.com/twbs/bootstrap/issues/1768
$this->registerJs(<<<JS
var shiftWindow = function() { scrollBy(0, -50) };
if (location.hash) shiftWindow();
window.addEventListener("hashchange", shiftWindow);
JS
,
\yii\web\View::POS_HEAD
);
$this->beginPage();
?>
<!DOCTYPE html>
......
......@@ -33,7 +33,7 @@ ArrayHelper::multisort($constants, 'name');
<tr<?= $constant->definedBy != $type->name ? ' class="inherited"' : '' ?> id="<?= $constant->name ?>">
<td><?= $constant->name ?><a name="<?= $constant->name ?>-detail"></a></td>
<td><?= $constant->value ?></td>
<td><?= APiMarkdown::process($constant->shortDescription . "\n" . $constant->description, $constant->definedBy, true) ?></td>
<td><?= ApiMarkdown::process($constant->shortDescription . "\n" . $constant->description, $constant->definedBy, true) ?></td>
<td><?= $this->context->typeLink($constant->definedBy) ?></td>
</tr>
<?php endforeach; ?>
......
......@@ -92,7 +92,7 @@ class Tabs extends Widget
*/
public $encodeLabels = true;
/**
* @var string, specifies the Bootstrap tab styling.
* @var string specifies the Bootstrap tab styling.
*/
public $navType = 'nav-tabs';
......
......@@ -36,7 +36,6 @@
border-right: 1px solid #e4e4e4;
padding: 4px 8px;
line-height: 32px;
height: 40px;
white-space: nowrap;
}
......
......@@ -97,7 +97,7 @@ class Mail extends Base
$dataProvider = new ArrayDataProvider([
'allModels' => $models,
'pagination' => [
'pageSize' => 5,
'pageSize' => 20,
],
'sort' => [
'attributes' => ['from', 'to', 'reply', 'cc', 'bcc', 'subject', 'body', 'charset'],
......
......@@ -9,8 +9,8 @@ use yii\helpers\Html;
<div class="yii-debug-toolbar-block">
<a href="<?= $panel->getUrl() ?>">
Yii
<span class="label label-info"><?= $panel->data['application']['yii'] ?></span>
<span class="label"><?= $panel->data['application']['yii'] ?></span>
PHP
<span class="label label-info"><?= $panel->data['php']['version'] ?></span>
<span class="label"><?= $panel->data['php']['version'] ?></span>
</a>
</div>
<?php if ($queryCount): ?>
<div class="yii-debug-toolbar-block">
<a href="<?= $panel->getUrl() ?>" title="Executed <?php echo $queryCount; ?> database queries which took <?= $queryTime ?>.">
DB <span class="label"><?= $queryCount ?></span> <span class="label"><?= $queryTime ?></span>
DB <span class="label label-info"><?= $queryCount ?></span> <span class="label"><?= $queryTime ?></span>
</a>
</div>
<?php endif; ?>
<div class="yii-debug-toolbar-block">
<a href="<?= $panel->getUrl() ?>" title="Total request processing time was <?= $time ?>">Time <span class="label"><?= $time ?></span></a>
<a href="<?= $panel->getUrl() ?>" title="Peak memory consumption">Memory <span class="label"><?= $memory ?></span></a>
<a href="<?= $panel->getUrl() ?>" title="Total request processing time was <?= $time ?>">Time <span class="label label-info"><?= $time ?></span></a>
<a href="<?= $panel->getUrl() ?>" title="Peak memory consumption">Memory <span class="label label-info"><?= $memory ?></span></a>
</div>
......@@ -6,7 +6,6 @@
* @var string $position
*/
use yii\helpers\Html;
use yii\debug\panels\ConfigPanel;
$minJs = <<<EOD
document.getElementById('yii-debug-toolbar').style.display = 'none';
......
......@@ -141,6 +141,7 @@ class Module extends \yii\base\Module
'controller' => ['class' => 'yii\gii\generators\controller\Generator'],
'form' => ['class' => 'yii\gii\generators\form\Generator'],
'module' => ['class' => 'yii\gii\generators\module\Generator'],
'extension' => ['class' => 'yii\gii\generators\extension\Generator'],
];
}
}
......@@ -16,6 +16,7 @@ use yii\helpers\Inflector;
use yii\web\Controller;
/**
* Generates CRUD
*
* @property array $columnNames Model column names. This property is read-only.
* @property string $controllerID The controller ID (without the module ID prefix). This property is
......
......@@ -31,7 +31,7 @@ namespace <?= StringHelper::dirname(ltrim($generator->controllerClass, '\\')) ?>
use Yii;
use <?= ltrim($generator->modelClass, '\\') ?>;
use <?= ltrim($generator->searchModelClass, '\\') . (isset($searchModelClass) ? " as $searchModelClass" : "") ?>;
use <?= ltrim($generator->searchModelClass, '\\') . (isset($searchModelAlias) ? " as $searchModelAlias" : "") ?>;
use <?= ltrim($generator->baseControllerClass, '\\') ?>;
use yii\web\NotFoundHttpException;
use yii\web\VerbFilter;
......
......@@ -72,7 +72,13 @@ class <?= $searchModelClass ?> extends Model
protected function addCondition($query, $attribute, $partialMatch = false)
{
$value = $this->$attribute;
if (($pos = strrpos($attribute, '.')) !== false) {
$modelAttribute = substr($attribute, $pos + 1);
} else {
$modelAttribute = $attribute;
}
$value = $this->$modelAttribute;
if (trim($value) === '') {
return;
}
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\gii\generators\extension;
use Yii;
use yii\gii\CodeFile;
/**
* This generator will generate the skeleton files needed by an extension.
*
* @author Tobias Munk <schmunk@usrbin.de>
* @since 2.0
*/
class Generator extends \yii\gii\Generator
{
public $vendorName;
public $packageName = "yii2-";
public $namespace;
public $type = "yii2-extension";
public $keywords = "yii2,extension";
public $title;
public $description;
public $outputPath = "@app/runtime/tmp-extensions";
public $license;
public $authorName;
public $authorEmail;
/**
* @inheritdoc
*/
public function getName()
{
return 'Extension Generator';
}
/**
* @inheritdoc
*/
public function getDescription()
{
return 'This generator helps you to generate the files needed by a Yii extension.';
}
/**
* @inheritdoc
*/
public function rules()
{
return array_merge(
parent::rules(),
[
[['vendorName', 'packageName'], 'filter', 'filter' => 'trim'],
[
[
'vendorName',
'packageName',
'namespace',
'type',
'license',
'title',
'description',
'authorName',
'authorEmail',
'outputPath'
],
'required'
],
[['keywords'], 'safe'],
[['authorEmail'], 'email'],
[
['vendorName', 'packageName'],
'match',
'pattern' => '/^[a-z0-9\-\.]+$/',
'message' => 'Only lowercase word characters, dashes and dots are allowed.'
],
[
['namespace'],
'match',
'pattern' => '/^[a-zA-Z0-9\\\]+\\\$/',
'message' => 'Only letters, numbers and backslashes are allowed. PSR-4 namespaces must end with a namespace separator.'
],
]
);
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'vendorName' => 'Vendor Name',
'packageName' => 'Package Name',
'license' => 'License',
];
}
/**
* @inheritdoc
*/
public function hints()
{
return [
'vendorName' => 'This refers to the name of the publisher, your GitHub user name is usually a good choice, eg. <code>myself</code>.',
'packageName' => 'This is the name of the extension on packagist, eg. <code>yii2-foobar</code>.',
'namespace' => 'PSR-4, eg. <code>myself\foobar\</code> This will be added to your autoloading by composer. Do not use yii or yii2 in the namespace.',
'keywords' => 'Comma separated keywords for this extension.',
'outputPath' => 'The temporary location of the generated files.',
'title' => 'A more descriptive name of your application for the README file.',
'description' => 'A sentence or subline describing the main purpose of the extension.',
];
}
/**
* @inheritdoc
*/
public function stickyAttributes()
{
return ['vendorName', 'outputPath', 'authorName', 'authorEmail'];
}
/**
* @inheritdoc
*/
public function successMessage()
{
$outputPath = realpath(\Yii::getAlias($this->outputPath));
$output1 = <<<EOD
<p><em>The extension has been generated successfully.</em></p>
<p>To enable it in your application, you need to create a git repository
and require it via composer.</p>
EOD;
$code1 = <<<EOD
cd {$outputPath}/{$this->packageName}
git init
git add -A
git commit
git remote add origin https://path.to/your/repo
git push -u origin master
EOD;
$output2 = <<<EOD
<p>The next step is just for <em>initial development</em>, skip it if you directly publish the extension on packagist.org</p>
<p>Add the newly created repo to your composer.json.</p>
EOD;
$code2 = <<<EOD
"repositories":[
{
"type": "git",
"url": "https://path.to/your/repo"
}
]
EOD;
$output3 = <<<EOD
<p class="well">Note: You may use the url <code>file://{$outputPath}/{$this->packageName}</code> for testing.</p>
<p>Require the package with composer</p>
EOD;
$code3 = <<<EOD
composer.phar require {$this->vendorName}/{$this->packageName}:dev-master
EOD;
$output4 = <<<EOD
<p>And use it in your application.</p>
EOD;
$code4 = <<<EOD
\\{$this->namespace}AutoloadExample::widget();
EOD;
$output5 = <<<EOD
<p>When you have finished development register your extension at <a href='https://packagist.org/' target='_blank'>packagist.org</a>.</p>
EOD;
$return = $output1 . '<pre>' . highlight_string($code1, true) . '</pre>';
$return .= $output2 . '<pre>' . highlight_string($code2, true) . '</pre>';
$return .= $output3 . '<pre>' . highlight_string($code3, true) . '</pre>';
$return .= $output4 . '<pre>' . highlight_string($code4, true) . '</pre>';
$return .= $output5;
return $return;
}
/**
* @inheritdoc
*/
public function requiredTemplates()
{
return ['composer.json', 'AutoloadExample.php', 'README.md'];
}
/**
* @inheritdoc
*/
public function generate()
{
$files = [];
$modulePath = $this->getOutputPath();
$files[] = new CodeFile(
$modulePath . '/' . $this->packageName . '/composer.json',
$this->render("composer.json")
);
$files[] = new CodeFile(
$modulePath . '/' . $this->packageName . '/AutoloadExample.php',
$this->render("AutoloadExample.php")
);
$files[] = new CodeFile(
$modulePath . '/' . $this->packageName . '/README.md',
$this->render("README.md")
);
return $files;
}
/**
* @return boolean the directory that contains the module class
*/
public function getOutputPath()
{
return Yii::getAlias($this->outputPath);
}
/**
* @return string a json encoded array with the given keywords
*/
public function getKeywordsArrayJson()
{
return json_encode(explode(',', $this->keywords));
}
/**
* @return array options for type drop-down
*/
public function optsType()
{
$licenses = [
'yii2-extension',
'library',
];
return array_combine($licenses, $licenses);
}
/**
* @return array options for license drop-down
*/
public function optsLicense()
{
$licenses = [
'Apache-2.0',
'BSD-2-Clause',
'BSD-3-Clause',
'BSD-4-Clause',
'GPL-2.0',
'GPL-2.0+',
'GPL-3.0',
'GPL-3.0+',
'LGPL-2.1',
'LGPL-2.1+',
'LGPL-3.0',
'LGPL-3.0+',
'MIT'
];
return array_combine($licenses, $licenses);
}
}
<?php
/**
* @var yii\web\View $this
* @var yii\widgets\ActiveForm $form
* @var yii\gii\generators\module\Generator $generator
*/
?>
<div class="alert alert-info">
Please read the
<?= \yii\helpers\Html::a('Extension Guidelines', 'https://github.com/yiisoft/yii2/blob/master/docs/guide/extensions.md', ['target'=>'new']) ?>
before creating an extension.
</div>
<div class="module-form">
<?php
echo $form->field($generator, 'vendorName');
echo $form->field($generator, 'packageName');
echo $form->field($generator, 'namespace');
echo $form->field($generator, 'type')->dropDownList($generator->optsType());
echo $form->field($generator, 'keywords');
echo $form->field($generator, 'license')->dropDownList($generator->optsLicense(), ['prompt'=>'Choose...']);
echo $form->field($generator, 'title');
echo $form->field($generator, 'description');
echo $form->field($generator, 'authorName');
echo $form->field($generator, 'authorEmail');
echo $form->field($generator, 'outputPath');
?>
</div>
<?php
/**
* This is just an example.
*/
echo "<?php\n";
?>
namespace <?= substr($generator->namespace, 0, -1) ?>;
class AutoloadExample extends \yii\base\widget {
function run() {
return "Hello!";
}
}
<?= $generator->title ?>
<?= str_repeat('=', mb_strlen($generator->title, \Yii::$app->charset)) ?>
<?= $generator->description ?>
Installation
------------
The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
Either run
```
php composer.phar require --prefer-dist <?= $generator->vendorName ?>/<?= $generator->packageName ?> "*"
```
or add
```
"<?= $generator->vendorName ?>/<?= $generator->packageName ?>": "*"
```
to the require section of your `composer.json` file.
Usage
-----
Once the extension is installed, simply use it in your code by :
```php
<?= "<?= \\{$generator->namespace}AutoloadExample::widget(); ?>" ?>
```
\ No newline at end of file
{
"name": "<?= $generator->vendorName ?>/<?= $generator->packageName ?>",
"description": "<?= $generator->description ?>",
"type": "<?= $generator->type ?>",
"keywords": <?= $generator->keywordsArrayJson ?>,
"license": "<?= $generator->license ?>",
"authors": [
{
"name": "<?= $generator->authorName ?>",
"email": "<?= $generator->authorEmail ?>"
}
],
"autoload": {
"psr-4": {
"<?= str_replace('\\','\\\\',$generator->namespace) ?>": ""
}
}
}
......@@ -6,6 +6,7 @@ Yii Framework 2 jui extension Change Log
- Bug #1550: fixed the issue that JUI input widgets did not property input IDs. (qiangxue)
- Bug #2514: Jui sortable clientEvents were not working because of wrong naming assumptions. (cebe)
- Enh #2573: Jui datepicker now uses the current appliaction language by default. (andy5)
2.0.0 alpha, December 1, 2013
-----------------------------
......
......@@ -7,11 +7,12 @@
namespace yii\jui;
use Yii;
use yii\helpers\Html;
use yii\helpers\Json;
/**
* DatePicker renders an datepicker jQuery UI widget.
* DatePicker renders a datepicker jQuery UI widget.
*
* For example:
*
......@@ -46,9 +47,9 @@ class DatePicker extends InputWidget
{
/**
* @var string the locale ID (eg 'fr', 'de') for the language to be used by the date picker.
* If this property set to false, I18N will not be involved. That is, the date picker will show in English.
* If this property is empty, then the current application language will be used.
*/
public $language = false;
public $language;
/**
* @var boolean If true, shows the widget as an inline calendar and the input as a hidden field.
*/
......@@ -77,15 +78,16 @@ class DatePicker extends InputWidget
{
echo $this->renderWidget() . "\n";
$containerID = $this->inline ? $this->containerOptions['id'] : $this->options['id'];
if ($this->language !== false) {
$language = $this->language ? $this->language : Yii::$app->language;
if ($language != 'en') {
$view = $this->getView();
DatePickerRegionalAsset::register($view);
$options = Json::encode($this->clientOptions);
$view->registerJs("$('#{$containerID}').datepicker($.extend({}, $.datepicker.regional['{$this->language}'], $options));");
$view->registerJs("$('#{$containerID}').datepicker($.extend({}, $.datepicker.regional['{$language}'], $options));");
$options = $this->clientOptions;
$this->clientOptions = false; // the datepicker js widget is already registered
$this->clientOptions = false; // the datepicker js widget is already registered
$this->registerWidget('datepicker', DatePickerAsset::className(), $containerID);
$this->clientOptions = $options;
} else {
......
......@@ -7,6 +7,7 @@
namespace yii\redis;
use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
use yii\db\Exception;
use yii\db\Expression;
......
......@@ -77,7 +77,7 @@ class QueryBuilder extends Object
$clauses = [
$this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
$this->buildFrom($from, $Params),
$this->buildFrom($from, $params),
$this->buildWhere($query->from, $query->where, $params),
$this->buildGroupBy($query->groupBy),
$this->buildWithin($query->within),
......
......@@ -48,6 +48,7 @@ Yii Framework 2 Change Log
- Bug #2399: Fixed the bug that AssetBundle did not handle relative URLs correctly (qiangxue)
- Bug #2502: Unclear error message when `$_SERVER['DOCUMENT_ROOT']` is empty (samdark)
- Bug #2519: MessageSource removed translation messages when event handler was bound to `missingTranslation`-event (cebe)
- Bug #2527: Source language for `app` message category was always `en` no matter which application `sourceLanguage` was used (samdark)
- Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark)
- Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark)
- Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe)
......@@ -57,6 +58,8 @@ Yii Framework 2 Change Log
- Bug: Fixed `HelpController::getModuleCommands` issue where it attempts to scan a module's controller directory when it doesn't exist (jom)
- Bug: Fixed an issue with Filehelper and not accessable directories which resulted in endless loop (cebe)
- Bug: Fixed `$model->load($data)` returned `true` if `$data` and `formName` were empty (samdark)
- Bug: Fixed issue with `ActiveRelationTrait` preventing `ActiveQuery` from clearing events and behaviors on clone (jom)
- Bug: `Query::queryScalar` wasn't making `SELECT DISTINCT` queries subqueries (jom)
- Enh #46: Added Image extension based on [Imagine library](http://imagine.readthedocs.org) (tonydspaniard)
- Enh #364: Improve Inflector::slug with `intl` transliteration. Improved transliteration char map. (tonydspaniard)
- Enh #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` (qiangxue)
......@@ -115,12 +118,16 @@ Yii Framework 2 Change Log
- Enh #2325: Adding support for the `X-HTTP-Method-Override` header in `yii\web\Request::getMethod()` (pawzar)
- Enh #2364: Take into account current error reporting level in error handler (gureedo)
- Enh #2387: Added support for fetching data from database in batches (nineinchnick, qiangxue)
- Enh #2392: Added `addCssStyle()`, `removeCssStyle()`, `cssStyleFromArray()` and `cssStyleToArray()` to `Html` (qiangxue, kartik-v, Alex-Code)
- Enh #2411: Added Gii extension generator (schmunk42)
- Enh #2415: Added support for inverse relations (qiangxue)
- Enh #2417: Added possibility to set `dataType` for `$.ajax` call in yii.activeForm.js (Borales)
- Enh #2436: Label of the attribute, which looks like `relatedModel.attribute`, will be received from the related model if it available (djagya)
- Enh #2415: Added support for inverse relations (qiangxue)
- Enh #2490: `yii\db\Query::count()` and other query scalar methods now properly handle queries with GROUP BY clause (qiangxue)
- Enh #2491: Added support for using the same base class name of search model and data model in Gii (qiangxue)
- Enh #2499: Added ability to downgrade migrations by their absolute apply time (resurtm, gorcer)
- Enh #2525: Added support for formatting file sizes with `yii\base\Formatter` (VinceG)
- Enh #2526: Allow for null values in batchInsert (skotos)
- Enh: Added support for using arrays as option values for console commands (qiangxue)
- Enh: Added `favicon.ico` and `robots.txt` to default application templates (samdark)
- Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue)
......@@ -139,6 +146,8 @@ Yii Framework 2 Change Log
- Enh: Added `yii\web\Response::clearOutputBuffers()` (qiangxue)
- Enh: Improved `QueryBuilder::buildLimit()` to support big numbers (qiangxue)
- Enh: Added support for building SQLs with sub-queries (qiangxue)
- Enh: Added `Pagination::getLinks()` (qiangxue)
- Enh: Added support for reading page size from query parameters by `Pagination` (qiangxue)
- Chg #1186: Changed `Sort` to use comma to separate multiple sort fields and use negative sign to indicate descending sort (qiangxue)
- Chg #1519: `yii\web\User::loginRequired()` now returns the `Response` object instead of exiting the application (qiangxue)
- Chg #1586: `QueryBuilder::buildLikeCondition()` will now escape special characters and use percentage characters by default (qiangxue)
......@@ -167,6 +176,7 @@ Yii Framework 2 Change Log
- Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe)
- Chg #2405: The CSS class of `MaskedInput` now defaults to `form-control` (qiangxue)
- Chg #2426: Changed URL creation method signatures to be consistent (samdark)
- Chg #2544: Changed `DetailView`'s `name:format:label` to `attribute:format:label` to match `GridView` (samdark)
- Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue)
- Chg: Renamed `ActiveRecord::getPopulatedRelations()` to `getRelatedRecords()` (qiangxue)
- Chg: Renamed `attributeName` and `className` to `targetAttribute` and `targetClass` for `UniqueValidator` and `ExistValidator` (qiangxue)
......@@ -198,7 +208,7 @@ Yii Framework 2 Change Log
- Renamed `yii\web\User::authTimeoutVar` to `authTimeoutParam`
- Renamed `yii\web\User::returnUrlVar` to `returnUrlParam`
- Chg: Added `View::viewFile` and removed `ViewEvent::viewFile` (qiangxue)
- Chg: Changed `Controller::afterAction()`, `Module::afterAction()` and `ActionFilter::afterAction()` to pass `$result` by value instead of reference (qiangxue)
- New #66: [Auth client library](https://github.com/yiisoft/yii2-authclient) OpenId, OAuth1, OAuth2 clients (klimov-paul)
- New #706: Added `yii\widgets\Pjax` and enhanced `GridView` to work with `Pjax` to support AJAX-update (qiangxue)
- New #1393: [Codeception testing framework integration](https://github.com/yiisoft/yii2-codeception) (Ragazzo)
......
Yii PHP Framework Version 2
===========================
This is the core framework code of [Yii 2](https://github.com/yiisoft/yii2).
This is the core framework code of [Yii 2](https://github.com/yiisoft/yii2#readme).
Installation
......
......@@ -84,7 +84,13 @@ class Action extends Component
if (Yii::$app->requestedParams === null) {
Yii::$app->requestedParams = $args;
}
return call_user_func_array([$this, 'run'], $args);
if ($this->beforeRun()) {
$result = call_user_func_array([$this, 'run'], $args);
$this->afterRun();
return $result;
} else {
return null;
}
}
/**
......
......@@ -65,7 +65,7 @@ class ActionFilter extends Behavior
public function afterFilter($event)
{
if ($this->isActive($event->action)) {
$this->afterAction($event->action, $event->result);
$event->result = $this->afterAction($event->action, $event->result);
}
}
......@@ -85,9 +85,11 @@ class ActionFilter extends Behavior
* You may override this method to do some postprocessing for the action.
* @param Action $action the action just executed.
* @param mixed $result the action execution result
* @return mixed the processed action result.
*/
public function afterAction($action, &$result)
public function afterAction($action, $result)
{
return $result;
}
/**
......
......@@ -126,11 +126,12 @@ class Controller extends Component implements ViewContextInterface
Yii::$app->trigger(Application::EVENT_BEFORE_ACTION, $event);
if ($event->isValid && $this->module->beforeAction($action) && $this->beforeAction($action)) {
$result = $action->runWithParams($params);
$this->afterAction($action, $result);
$this->module->afterAction($action, $result);
$result = $this->afterAction($action, $result);
$result = $this->module->afterAction($action, $result);
$event = new ActionEvent($action);
$event->result = &$result;
$event->result = $result;
Yii::$app->trigger(Application::EVENT_AFTER_ACTION, $event);
$result = $event->result;
}
$this->action = $oldAction;
return $result;
......@@ -222,14 +223,17 @@ class Controller extends Component implements ViewContextInterface
* This method is invoked right after an action is executed.
* You may override this method to do some postprocessing for the action.
* If you override this method, please make sure you call the parent implementation first.
* Also make sure you return the action result, whether it is processed or not.
* @param Action $action the action just executed.
* @param mixed $result the action return result.
* @return mixed the processed action result.
*/
public function afterAction($action, &$result)
public function afterAction($action, $result)
{
$event = new ActionEvent($action);
$event->result = &$result;
$event->result = $result;
$this->trigger(self::EVENT_AFTER_ACTION, $event);
return $event->result;
}
/**
......
......@@ -66,6 +66,16 @@ class Formatter extends Component
* If not set, "," will be used.
*/
public $thousandSeparator;
/**
* @var array the format used to format size (bytes). Three elements may be specified: "base", "decimals" and "decimalSeparator".
* They correspond to the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte, defaults to 1024),
* the number of digits after the decimal point (defaults to 2) and the character displayed as the decimal point.
*/
public $sizeFormat = [
'base' => 1024,
'decimals' => 2,
'decimalSeparator' => null,
];
/**
* Initializes the component.
......@@ -111,7 +121,7 @@ class Formatter extends Component
$params = [$value];
}
$method = 'as' . $format;
if (method_exists($this, $method)) {
if ($this->hasMethod($method)) {
return call_user_func_array([$this, $method], $params);
} else {
throw new InvalidParamException("Unknown type: $format");
......@@ -269,7 +279,7 @@ class Formatter extends Component
return $this->nullDisplay;
}
$value = $this->normalizeDatetimeValue($value);
return $this->formatTimestamp($value, $format === null ? $this->dateFormat : $format, $value);
return $this->formatTimestamp($value, $format === null ? $this->dateFormat : $format);
}
/**
......@@ -293,7 +303,7 @@ class Formatter extends Component
return $this->nullDisplay;
}
$value = $this->normalizeDatetimeValue($value);
return $this->formatTimestamp($value, $format === null ? $this->timeFormat : $format, $value);
return $this->formatTimestamp($value, $format === null ? $this->timeFormat : $format);
}
/**
......@@ -317,7 +327,7 @@ class Formatter extends Component
return $this->nullDisplay;
}
$value = $this->normalizeDatetimeValue($value);
return $this->formatTimestamp($value, $format === null ? $this->datetimeFormat : $format, $value);
return $this->formatTimestamp($value, $format === null ? $this->datetimeFormat : $format);
}
/**
......@@ -404,4 +414,45 @@ class Formatter extends Component
$ts = isset($this->thousandSeparator) ? $this->thousandSeparator: ',';
return number_format($value, $decimals, $ds, $ts);
}
/**
* Formats the value in bytes as a size in human readable form.
* @param integer $value value in bytes to be formatted
* @param boolean $verbose if full names should be used (e.g. bytes, kilobytes, ...).
* Defaults to false meaning that short names will be used (e.g. B, KB, ...).
* @return string the formatted result
* @see sizeFormat
*/
public function asSize($value, $verbose = false)
{
$position = 0;
do {
if ($value < $this->sizeFormat['base']) {
break;
}
$value = $value / $this->sizeFormat['base'];
$position++;
} while ($position < 6);
$value = round($value, $this->sizeFormat['decimals']);
$formattedValue = isset($this->sizeFormat['decimalSeparator']) ? str_replace('.', $this->sizeFormat['decimalSeparator'], $value) : $value;
$params = ['n' => $formattedValue];
switch($position) {
case 0:
return $verbose ? Yii::t('yii','{n, plural, =1{# byte} other{# bytes}}', $params) : Yii::t('yii', '{n} B', $params);
case 1:
return $verbose ? Yii::t('yii','{n, plural, =1{# kilobyte} other{# kilobytes}}', $params) : Yii::t('yii','{n} KB', $params);
case 2:
return $verbose ? Yii::t('yii','{n, plural, =1{# megabyte} other{# megabytes}}', $params) : Yii::t('yii','{n} MB', $params);
case 3:
return $verbose ? Yii::t('yii','{n, plural, =1{# gigabyte} other{# gigabytes}}', $params) : Yii::t('yii','{n} GB', $params);
case 4:
return $verbose ? Yii::t('yii','{n, plural, =1{# terabyte} other{# terabytes}}', $params) : Yii::t('yii','{n} TB', $params);
default:
return $verbose ? Yii::t('yii','{n, plural, =1{# petabyte} other{# petabytes}}', $params) : Yii::t('yii','{n} PB', $params);
}
}
}
......@@ -658,10 +658,13 @@ class Module extends Component
* This method is invoked right after an action of this module has been executed.
* You may override this method to do some postprocessing for the action.
* Make sure you call the parent implementation so that the relevant event is triggered.
* Also make sure you return the action result, whether it is processed or not.
* @param Action $action the action just executed.
* @param mixed $result the action return result.
* @return mixed the processed action result.
*/
public function afterAction($action, &$result)
public function afterAction($action, $result)
{
return $result;
}
}
......@@ -188,6 +188,7 @@ abstract class BaseDataProvider extends Component implements DataProviderInterfa
$config = ['class' => Pagination::className()];
if ($this->id !== null) {
$config['pageParam'] = $this->id . '-page';
$config['pageSizeParam'] = $this->id . '-per-page';
}
$this->_pagination = Yii::createObject(array_merge($config, $value));
} elseif ($value instanceof Pagination || $value === false) {
......
......@@ -67,12 +67,23 @@ use yii\web\Request;
*/
class Pagination extends Object
{
const LINK_SELF = 'self';
const LINK_NEXT = 'next';
const LINK_PREV = 'prev';
const LINK_FIRST = 'first';
const LINK_LAST = 'last';
/**
* @var string name of the parameter storing the current page index. Defaults to 'page'.
* @var string name of the parameter storing the current page index.
* @see params
*/
public $pageParam = 'page';
/**
* @var string name of the parameter storing the page size.
* @see params
*/
public $pageSizeParam = 'per-page';
/**
* @var boolean whether to always have the page parameter in the URL created by [[createUrl()]].
* If false and [[page]] is 0, the page parameter will not be put in the URL.
*/
......@@ -88,8 +99,8 @@ class Pagination extends Object
*
* In order to add hash to all links use `array_merge($_GET, ['#' => 'my-hash'])`.
*
* The array element indexed by [[pageParam]] is considered to be the current page number.
* If the element does not exist, the current page number is considered 0.
* The array element indexed by [[pageParam]] is considered to be the current page number (defaults to 0);
* while the element indexed by [[pageSizeParam]] is treated as the page size (defaults to [[defaultPageSize]]).
*/
public $params;
/**
......@@ -106,14 +117,24 @@ class Pagination extends Object
*/
public $validatePage = true;
/**
* @var integer number of items on each page. Defaults to 20.
* If it is less than 1, it means the page size is infinite, and thus a single page contains all items.
*/
public $pageSize = 20;
/**
* @var integer total number of items.
*/
public $totalCount = 0;
/**
* @var integer the default page size. This property will be returned by [[pageSize]] when page size
* cannot be determined by [[pageSizeParam]] from [[params]].
*/
public $defaultPageSize = 20;
/**
* @var array|boolean the page size limits. The first array element stands for the minimal page size, and the second
* the maximal page size. If this is false, it means [[pageSize]] should always return the value of [[defaultPageSize]].
*/
public $pageSizeLimit = [1, 50];
/**
* @var integer number of items on each page.
* If it is less than 1, it means the page size is infinite, and thus a single page contains all items.
*/
private $_pageSize;
/**
......@@ -121,11 +142,12 @@ class Pagination extends Object
*/
public function getPageCount()
{
if ($this->pageSize < 1) {
$pageSize = $this->getPageSize();
if ($pageSize < 1) {
return $this->totalCount > 0 ? 1 : 0;
} else {
$totalCount = $this->totalCount < 0 ? 0 : (int)$this->totalCount;
return (int)(($totalCount + $this->pageSize - 1) / $this->pageSize);
return (int)(($totalCount + $pageSize - 1) / $pageSize);
}
}
......@@ -139,24 +161,8 @@ class Pagination extends Object
public function getPage($recalculate = false)
{
if ($this->_page === null || $recalculate) {
if (($params = $this->params) === null) {
$request = Yii::$app->getRequest();
$params = $request instanceof Request ? $request->getQueryParams() : [];
}
if (isset($params[$this->pageParam]) && is_scalar($params[$this->pageParam])) {
$this->_page = (int)$params[$this->pageParam] - 1;
if ($this->validatePage) {
$pageCount = $this->getPageCount();
if ($this->_page >= $pageCount) {
$this->_page = $pageCount - 1;
}
}
if ($this->_page < 0) {
$this->_page = 0;
}
} else {
$this->_page = 0;
}
$page = (int)$this->getQueryParam($this->pageParam, 1) - 1;
$this->setPage($page, true);
}
return $this->_page;
}
......@@ -164,10 +170,68 @@ class Pagination extends Object
/**
* Sets the current page number.
* @param integer $value the zero-based index of the current page.
* @param boolean $validatePage whether to validate the page number. Note that in order
* to validate the page number, both [[validatePage]] and this parameter must be true.
*/
public function setPage($value)
public function setPage($value, $validatePage = false)
{
$this->_page = $value;
if ($value === null) {
$this->_page = null;
} else {
$value = (int)$value;
if ($validatePage && $this->validatePage) {
$pageCount = $this->getPageCount();
if ($value >= $pageCount) {
$value = $pageCount - 1;
}
}
if ($value < 0) {
$value = 0;
}
$this->_page = $value;
}
}
/**
* Returns the number of items per page.
* By default, this method will try to determine the page size by [[pageSizeParam]] in [[params]].
* If the page size cannot be determined this way, [[defaultPageSize]] will be returned.
* @return integer the number of items per page.
* @see pageSizeLimit
*/
public function getPageSize()
{
if ($this->_pageSize === null) {
if (empty($this->pageSizeLimit)) {
$pageSize = $this->defaultPageSize;
$this->setPageSize($pageSize);
} else {
$pageSize = (int)$this->getQueryParam($this->pageSizeParam, $this->defaultPageSize);
$this->setPageSize($pageSize, true);
}
}
return $this->_pageSize;
}
/**
* @param integer $value the number of items per page.
* @param boolean $validatePageSize whether to validate page size.
*/
public function setPageSize($value, $validatePageSize = false)
{
if ($value === null) {
$this->_pageSize = null;
} else {
$value = (int)$value;
if ($validatePageSize && count($this->pageSizeLimit) === 2 && isset($this->pageSizeLimit[0], $this->pageSizeLimit[1])) {
if ($value < $this->pageSizeLimit[0]) {
$value = $this->pageSizeLimit[0];
} elseif ($value > $this->pageSizeLimit[1]) {
$value = $this->pageSizeLimit[1];
}
}
$this->_pageSize = $value;
}
}
/**
......@@ -190,6 +254,12 @@ class Pagination extends Object
} else {
unset($params[$this->pageParam]);
}
$pageSize = $this->getPageSize();
if ($pageSize != $this->defaultPageSize) {
$params[$this->pageSizeParam] = $pageSize;
} else {
unset($params[$this->pageSizeParam]);
}
$params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route;
$urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager;
if ($absolute) {
......@@ -205,7 +275,8 @@ class Pagination extends Object
*/
public function getOffset()
{
return $this->pageSize < 1 ? 0 : $this->getPage() * $this->pageSize;
$pageSize = $this->getPageSize();
return $pageSize < 1 ? 0 : $this->getPage() * $pageSize;
}
/**
......@@ -215,6 +286,47 @@ class Pagination extends Object
*/
public function getLimit()
{
return $this->pageSize < 1 ? -1 : $this->pageSize;
$pageSize = $this->getPageSize();
return $pageSize < 1 ? -1 : $pageSize;
}
/**
* Returns a whole set of links for navigating to the first, last, next and previous pages.
* @param boolean $absolute whether the generated URLs should be absolute.
* @return array the links for navigational purpose. The array keys specify the purpose of the links (e.g. [[LINK_FIRST]]),
* and the array values are the corresponding URLs.
*/
public function getLinks($absolute = false)
{
$currentPage = $this->getPage();
$pageCount = $this->getPageCount();
$links = [
self::LINK_SELF => $this->createUrl($currentPage, $absolute),
];
if ($currentPage > 0) {
$links[self::LINK_FIRST] = $this->createUrl(0, $absolute);
$links[self::LINK_PREV] = $this->createUrl($currentPage - 1, $absolute);
}
if ($currentPage < $pageCount - 1) {
$links[self::LINK_NEXT] = $this->createUrl($currentPage + 1, $absolute);
$links[self::LINK_LAST] = $this->createUrl($pageCount - 1, $absolute);
}
return $links;
}
/**
* Returns the value of the specified query parameter.
* This method returns the named parameter value from [[params]]. Null is returned if the value does not exist.
* @param string $name the parameter name
* @param string $defaultValue the value to be returned when the specified parameter does not exist in [[params]].
* @return string the parameter value
*/
protected function getQueryParam($name, $defaultValue = null)
{
if (($params = $this->params) === null) {
$request = Yii::$app->getRequest();
$params = $request instanceof Request ? $request->getQueryParams() : [];
}
return isset($params[$name]) && is_scalar($params[$name]) ? $params[$name] : $defaultValue;
}
}
......@@ -377,7 +377,7 @@ class ActiveRecord extends BaseActiveRecord
return false;
}
$db = static::getDb();
if ($this->isTransactional(self::OP_INSERT) && $db->getTransaction() === null) {
if ($this->isTransactional(self::OP_INSERT)) {
$transaction = $db->beginTransaction();
try {
$result = $this->insertInternal($attributes);
......@@ -397,9 +397,12 @@ class ActiveRecord extends BaseActiveRecord
}
/**
* @see ActiveRecord::insert()
* Inserts an ActiveRecord into DB without considering transaction.
* @param array $attributes list of attributes that need to be saved. Defaults to null,
* meaning all attributes that are loaded from DB will be saved.
* @return boolean whether the record is inserted successfully.
*/
private function insertInternal($attributes = null)
protected function insertInternal($attributes = null)
{
if (!$this->beforeSave(true)) {
return false;
......@@ -489,7 +492,7 @@ class ActiveRecord extends BaseActiveRecord
return false;
}
$db = static::getDb();
if ($this->isTransactional(self::OP_UPDATE) && $db->getTransaction() === null) {
if ($this->isTransactional(self::OP_UPDATE)) {
$transaction = $db->beginTransaction();
try {
$result = $this->updateInternal($attributes);
......@@ -530,36 +533,49 @@ class ActiveRecord extends BaseActiveRecord
public function delete()
{
$db = static::getDb();
$transaction = $this->isTransactional(self::OP_DELETE) && $db->getTransaction() === null ? $db->beginTransaction() : null;
try {
$result = false;
if ($this->beforeDelete()) {
// we do not check the return value of deleteAll() because it's possible
// the record is already deleted in the database and thus the method will return 0
$condition = $this->getOldPrimaryKey(true);
$lock = $this->optimisticLock();
if ($lock !== null) {
$condition[$lock] = $this->$lock;
}
$result = $this->deleteAll($condition);
if ($lock !== null && !$result) {
throw new StaleObjectException('The object being deleted is outdated.');
}
$this->setOldAttributes(null);
$this->afterDelete();
}
if ($transaction !== null) {
if ($this->isTransactional(self::OP_DELETE)) {
$transaction = $db->beginTransaction();
try {
$result = $this->deleteInternal();
if ($result === false) {
$transaction->rollBack();
} else {
$transaction->commit();
}
}
} catch (\Exception $e) {
if ($transaction !== null) {
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}
} else {
$result = $this->deleteInternal();
}
return $result;
}
/**
* Deletes an ActiveRecord without considering transaction.
* @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason.
* Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
* @throws StaleObjectException
*/
protected function deleteInternal()
{
$result = false;
if ($this->beforeDelete()) {
// we do not check the return value of deleteAll() because it's possible
// the record is already deleted in the database and thus the method will return 0
$condition = $this->getOldPrimaryKey(true);
$lock = $this->optimisticLock();
if ($lock !== null) {
$condition[$lock] = $this->$lock;
}
$result = $this->deleteAll($condition);
if ($lock !== null && !$result) {
throw new StaleObjectException('The object being deleted is outdated.');
}
throw $e;
$this->setOldAttributes(null);
$this->afterDelete();
}
return $result;
}
......
......@@ -64,6 +64,7 @@ trait ActiveRelationTrait
*/
public function __clone()
{
parent::__clone();
// make a clone of "via" object so that the same query object can be reused multiple times
if (is_object($this->via)) {
$this->via = clone $this->via;
......@@ -135,13 +136,13 @@ trait ActiveRelationTrait
* Finds the related records for the specified primary record.
* This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
* @param string $name the relation name
* @param ActiveRecordInterface $model the primary model
* @param ActiveRecordInterface|BaseActiveRecord $model the primary model
* @return mixed the related record(s)
* @throws InvalidParamException if the relation is invalid
*/
public function findFor($name, $model)
{
if (method_exists($model, 'get' . $name)) {
if ($model->hasMethod('get' . $name)) {
$method = new \ReflectionMethod($model, 'get' . $name);
$realName = lcfirst(substr($method->getName(), 3));
if ($realName !== $name) {
......
......@@ -1274,8 +1274,8 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
/**
* @param array $link
* @param ActiveRecord $foreignModel
* @param ActiveRecord $primaryModel
* @param BaseActiveRecord $foreignModel
* @param BaseActiveRecord $primaryModel
* @throws InvalidCallException
*/
private function bindModels($link, $foreignModel, $primaryModel)
......
......@@ -139,7 +139,7 @@ class Migration extends \yii\base\Component
{
echo " > execute SQL: $sql ...";
$time = microtime(true);
$this->db->createCommand($sql)->execute($params);
$this->db->createCommand($sql)->bindValues($params)->execute();
echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n";
}
......
......@@ -338,7 +338,7 @@ class Query extends Component implements QueryInterface
* Queries a scalar value by setting [[select]] first.
* Restores the value of select to make this query reusable.
* @param string|Expression $selectExpression
* @param Connection $db
* @param Connection|null $db
* @return bool|string
*/
private function queryScalar($selectExpression, $db)
......@@ -356,7 +356,7 @@ class Query extends Component implements QueryInterface
$this->limit = $limit;
$this->offset = $offset;
if (empty($this->groupBy)) {
if (empty($this->groupBy) && !$this->distinct) {
return $command->queryScalar();
} else {
return (new Query)->select([$selectExpression])
......
......@@ -177,7 +177,7 @@ class QueryBuilder extends \yii\base\Object
if (!is_array($value) && isset($columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->typecast($value);
}
$vs[] = is_string($value) ? $this->db->quoteValue($value) : $value;
$vs[] = is_string($value) ? $this->db->quoteValue($value) : ($value === null ? 'NULL' : $value);
}
$values[] = '(' . implode(', ', $vs) . ')';
}
......@@ -645,7 +645,7 @@ class QueryBuilder extends \yii\base\Object
}
/**
* @param string|array $joins
* @param array $joins
* @param array $params the binding parameters to be populated
* @return string the JOIN clause built from [[Query::$join]].
* @throws Exception if the $joins parameter is not in proper format
......
......@@ -368,7 +368,7 @@ SQL;
}
$sql = <<<SQL
SELECT [t].[table]
SELECT [t].[table_name]
FROM [information_schema].[tables] AS [t]
WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'BASE TABLE'
SQL;
......
......@@ -116,22 +116,35 @@ class Column extends Object
}
/**
* Renders the data cell content.
* Returns the raw data cell content.
* This method is called by [[renderDataCellContent()]] when rendering the content of a data cell.
* @param mixed $model the data model
* @param mixed $key the key associated with the data model
* @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]].
* @return string the rendering result
*/
protected function renderDataCellContent($model, $key, $index)
protected function getDataCellContent($model, $key, $index)
{
if ($this->content !== null) {
return call_user_func($this->content, $model, $key, $index, $this);
} else {
return $this->grid->emptyCell;
return null;
}
}
/**
* Renders the data cell content.
* @param mixed $model the data model
* @param mixed $key the key associated with the data model
* @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]].
* @return string the rendering result
*/
protected function renderDataCellContent($model, $key, $index)
{
return $this->content !== null ? $this->getDataCellContent($model, $key, $index) : $this->grid->emptyCell;
}
/**
* Renders the filter cell content.
* The default implementation simply renders a space.
* This method may be overridden to customize the rendering of the filter cell (if any).
......
......@@ -138,7 +138,7 @@ class DataColumn extends Column
/**
* @inheritdoc
*/
protected function renderDataCellContent($model, $key, $index)
protected function getDataCellContent($model, $key, $index)
{
if ($this->value !== null) {
if (is_string($this->value)) {
......@@ -149,8 +149,16 @@ class DataColumn extends Column
} elseif ($this->content === null && $this->attribute !== null) {
$value = ArrayHelper::getValue($model, $this->attribute);
} else {
return parent::renderDataCellContent($model, $key, $index);
return parent::getDataCellContent($model, $key, $index);
}
return $this->grid->formatter->format($value, $this->format);
return $value;
}
/**
* @inheritdoc
*/
protected function renderDataCellContent($model, $key, $index)
{
return $this->grid->formatter->format($this->getDataCellContent($model, $key, $index), $this->format);
}
}
......@@ -1608,6 +1608,122 @@ class BaseHtml
}
/**
* Adds the specified CSS style to the HTML options.
*
* If the options already contain a `style` element, the new style will be merged
* with the existing one. If a CSS property exists in both the new and the old styles,
* the old one may be overwritten if `$overwrite` is true.
*
* For example,
*
* ```php
* Html::addCssStyle($options, 'width: 100px; height: 200px');
* ```
*
* @param array $options the HTML options to be modified.
* @param string|array $style the new style string (e.g. `'width: 100px; height: 200px'`) or
* array (e.g. `['width' => '100px', 'height' => '200px']`).
* @param boolean $overwrite whether to overwrite existing CSS properties if the new style
* contain them too.
* @see removeCssStyle()
* @see cssStyleFromArray()
* @see cssStyleToArray()
*/
public static function addCssStyle(&$options, $style, $overwrite = true)
{
if (!empty($options['style'])) {
$oldStyle = static::cssStyleToArray($options['style']);
$newStyle = is_array($style) ? $style : static::cssStyleToArray($style);
if (!$overwrite) {
foreach ($newStyle as $property => $value) {
if (isset($oldStyle[$property])) {
unset($newStyle[$property]);
}
}
}
$style = static::cssStyleFromArray(array_merge($oldStyle, $newStyle));
}
$options['style'] = $style;
}
/**
* Removes the specified CSS style from the HTML options.
*
* For example,
*
* ```php
* Html::removeCssStyle($options, ['width', 'height']);
* ```
*
* @param array $options the HTML options to be modified.
* @param string|array $properties the CSS properties to be removed. You may use a string
* if you are removing a single property.
* @see addCssStyle()
*/
public static function removeCssStyle(&$options, $properties)
{
if (!empty($options['style'])) {
$style = static::cssStyleToArray($options['style']);
foreach ((array)$properties as $property) {
unset($style[$property]);
}
$options['style'] = static::cssStyleFromArray($style);
}
}
/**
* Converts a CSS style array into a string representation.
*
* For example,
*
* ```php
* print_r(Html::cssStyleFromArray(['width' => '100px', 'height' => '200px']));
* // will display: 'width: 100px; height: 200px;'
* ```
*
* @param array $style the CSS style array. The array keys are the CSS property names,
* and the array values are the corresponding CSS property values.
* @return string the CSS style string. If the CSS style is empty, a null will be returned.
*/
public static function cssStyleFromArray(array $style)
{
$result = '';
foreach ($style as $name => $value) {
$result .= "$name: $value; ";
}
// return null if empty to avoid rendering the "style" attribute
return $result === '' ? null : rtrim($result);
}
/**
* Converts a CSS style string into an array representation.
*
* The array keys are the CSS property names, and the array values
* are the corresponding CSS property values.
*
* For example,
*
* ```php
* print_r(Html::cssStyleToArray('width: 100px; height: 200px;'));
* // will display: ['width' => '100px', 'height' => '200px']
* ```
*
* @param string $style the CSS style string
* @return array the array representation of the CSS style
*/
public static function cssStyleToArray($style)
{
$result = [];
foreach (explode(';', $style) as $property) {
$property = explode(':', $property);
if (count($property) > 1) {
$result[trim($property[0])] = trim($property[1]);
}
}
return $result;
}
/**
* Returns the real attribute name from the given attribute expression.
*
* An attribute expression is an attribute name prefixed and/or suffixed with array indexes.
......
......@@ -65,7 +65,7 @@ class I18N extends Component
if (!isset($this->translations['app'])) {
$this->translations['app'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en',
'sourceLanguage' => Yii::$app->sourceLanguage,
'basePath' => '@app/messages',
];
}
......
......@@ -136,7 +136,7 @@ class Validator extends Component
{
$params['attributes'] = $attributes;
if ($type instanceof \Closure || method_exists($object, $type)) {
if ($type instanceof \Closure || $object->hasMethod($type)) {
// method-based validator
$params['class'] = __NAMESPACE__ . '\InlineValidator';
$params['method'] = $type;
......
......@@ -81,6 +81,7 @@ html,body{
}
.header h2{
font-size: 20px;
line-height: 1.25;
}
/* previous exceptions */
......@@ -360,9 +361,7 @@ pre .diff .change{
?></h1>
<?php endif; ?>
<h2><?= nl2br($handler->htmlEncode($exception->getMessage())) ?></h2>
<?php if ($exception instanceof \yii\db\Exception && $exception->errorInfo !== null): ?>
<pre><?= var_export($exception->errorInfo, true) ?></pre>
<?php endif; ?>
<?= $handler->renderPreviousExceptions($exception) ?>
</div>
......
......@@ -112,7 +112,7 @@ class AssetBundle extends Object
/**
* @param View $view
* @return AssetBundle the registered asset bundle instance
* @return static the registered asset bundle instance
*/
public static function register($view)
{
......
......@@ -136,15 +136,12 @@ class PageCache extends ActionFilter
}
/**
* This method is invoked right after an action is executed.
* You may override this method to do some postprocessing for the action.
* @param Action $action the action just executed.
* @param mixed $result the action execution result
* @inheritdoc
*/
public function afterAction($action, &$result)
public function afterAction($action, $result)
{
echo $result;
$this->view->endCache();
$result = ob_get_clean();
return ob_get_clean();
}
}
......@@ -292,6 +292,15 @@ class Request extends \yii\base\Request
}
/**
* Returns whether this is a PJAX request
* @return boolean whether this is a PJAX request
*/
public function getIsPjax ()
{
return $this->getIsAjax() && !empty($_SERVER['HTTP_X_PJAX']);
}
/**
* Returns whether this is an Adobe Flash or Flex request.
* @return boolean whether this is an Adobe Flash or Adobe Flex request.
*/
......@@ -876,9 +885,23 @@ class Request extends \yii\base\Request
/**
* Returns the content types acceptable by the end user.
* This is determined by the `Accept` HTTP header.
* @return array the content types ordered by the preference level. The first element
* represents the most preferred content type.
* This is determined by the `Accept` HTTP header. For example,
*
* ```php
* $_SERVER['HTTP_ACCEPT'] = 'text/plain; q=0.5, application/json; version=1.0, application/xml; version=2.0;';
* $types = $request->getAcceptableContentTypes();
* print_r($types);
* // displays:
* // [
* // 'application/json' => ['q' => 1, 'version' => '1.0'],
* // 'application/xml' => ['q' => 1, 'version' => '2.0'],
* // 'text/plain' => ['q' => 0.5],
* // ]
* ```
*
* @return array the content types ordered by the quality score. Types with the highest scores
* will be returned first. The array keys are the content types, while the array values
* are the corresponding quality score and other parameters as given in the header.
*/
public function getAcceptableContentTypes()
{
......@@ -893,8 +916,12 @@ class Request extends \yii\base\Request
}
/**
* Sets the acceptable content types.
* Please refer to [[getAcceptableContentTypes()]] on the format of the parameter.
* @param array $value the content types that are acceptable by the end user. They should
* be ordered by the preference level.
* @see getAcceptableContentTypes()
* @see parseAcceptHeader()
*/
public function setAcceptableContentTypes($value)
{
......@@ -928,7 +955,7 @@ class Request extends \yii\base\Request
{
if ($this->_languages === null) {
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$this->_languages = $this->parseAcceptHeader($_SERVER['HTTP_ACCEPT_LANGUAGE']);
$this->_languages = array_keys($this->parseAcceptHeader($_SERVER['HTTP_ACCEPT_LANGUAGE']));
} else {
$this->_languages = [];
}
......@@ -947,45 +974,86 @@ class Request extends \yii\base\Request
/**
* Parses the given `Accept` (or `Accept-Language`) header.
* This method will return the acceptable values ordered by their preference level.
*
* This method will return the acceptable values with their quality scores and the corresponding parameters
* as specified in the given `Accept` header. The array keys of the return value are the acceptable values,
* while the array values consisting of the corresponding quality scores and parameters. The acceptable
* values with the highest quality scores will be returned first. For example,
*
* ```php
* $header = 'text/plain; q=0.5, application/json; version=1.0, application/xml; version=2.0;';
* $accepts = $request->parseAcceptHeader($header);
* print_r($accepts);
* // displays:
* // [
* // 'application/json' => ['q' => 1, 'version' => '1.0'],
* // 'application/xml' => ['q' => 1, 'version' => '2.0'],
* // 'text/plain' => ['q' => 0.5],
* // ]
* ```
*
* @param string $header the header to be parsed
* @return array the accept values ordered by their preference level.
* @return array the acceptable values ordered by their quality score. The values with the highest scores
* will be returned first.
*/
protected function parseAcceptHeader($header)
public function parseAcceptHeader($header)
{
$accepts = [];
$n = preg_match_all('/\s*([\w\/\-\*]+)\s*(?:;\s*q\s*=\s*([\d\.]+))?[^,]*/', $header, $matches, PREG_SET_ORDER);
for ($i = 0; $i < $n; ++$i) {
if (!empty($matches[$i][1])) {
$accepts[] = [$matches[$i][1], isset($matches[$i][2]) ? (float)$matches[$i][2] : 1, $i];
foreach (explode(',', $header) as $i => $part) {
$params = preg_split('/\s*;\s*/', trim($part), -1, PREG_SPLIT_NO_EMPTY);
if (empty($params)) {
continue;
}
$values = [
'q' => [$i, array_shift($params), 1],
];
foreach ($params as $param) {
if (strpos($param, '=') !== false) {
list ($key, $value) = explode('=', $param, 2);
if ($key === 'q') {
$values['q'][2] = (double)$value;
} else {
$values[$key] = $value;
}
} else {
$values[] = $param;
}
}
$accepts[] = $values;
}
usort($accepts, function ($a, $b) {
if ($a[1] > $b[1]) {
$a = $a['q']; // index, name, q
$b = $b['q'];
if ($a[2] > $b[2]) {
return -1;
} elseif ($a[1] < $b[1]) {
} elseif ($a[2] < $b[2]) {
return 1;
} elseif ($a[0] === $b[0]) {
return $a[2] > $b[2] ? 1 : -1;
} elseif ($a[0] === '*/*') {
} elseif ($a[1] === $b[1]) {
return $a[0] > $b[0] ? 1 : -1;
} elseif ($a[1] === '*/*') {
return 1;
} elseif ($b[0] === '*/*') {
} elseif ($b[1] === '*/*') {
return -1;
} else {
$wa = $a[0][strlen($a[0]) - 1] === '*';
$wb = $b[0][strlen($b[0]) - 1] === '*';
$wa = $a[1][strlen($a[1]) - 1] === '*';
$wb = $b[1][strlen($b[1]) - 1] === '*';
if ($wa xor $wb) {
return $wa ? 1 : -1;
} else {
return $a[2] > $b[2] ? 1 : -1;
return $a[0] > $b[0] ? 1 : -1;
}
}
});
$result = [];
foreach ($accepts as $accept) {
$result[] = $accept[0];
$name = $accept['q'][1];
$accept['q'] = $accept['q'][2];
$result[$name] = $accept;
}
return array_unique($result);
return $result;
}
/**
......
......@@ -678,7 +678,9 @@ class Response extends \yii\base\Response
$url = Yii::$app->getRequest()->getHostInfo() . $url;
}
if (Yii::$app->getRequest()->getIsAjax()) {
if (Yii::$app->getRequest()->getIsPjax()) {
$this->getHeaders()->set('X-Pjax-Url', $url);
} elseif (Yii::$app->getRequest()->getIsAjax()) {
$this->getHeaders()->set('X-Redirect', $url);
} else {
$this->getHeaders()->set('Location', $url);
......
......@@ -57,15 +57,15 @@ class DetailView extends Widget
* @var array a list of attributes to be displayed in the detail view. Each array element
* represents the specification for displaying one particular attribute.
*
* An attribute can be specified as a string in the format of "name", "name:format" or "name:format:label",
* where "name" refers to the attribute name, and "format" represents the format of the attribute. The "format"
* An attribute can be specified as a string in the format of "attribute", "attribute:format" or "attribute:format:label",
* where "attribute" refers to the attribute name, and "format" represents the format of the attribute. The "format"
* is passed to the [[Formatter::format()]] method to format an attribute value into a displayable text.
* Please refer to [[Formatter]] for the supported types. Both "format" and "label" are optional.
* They will take default values if absent.
*
* An attribute can also be specified in terms of an array with the following elements:
*
* - name: the attribute name. This is required if either "label" or "value" is not specified.
* - attribute: the attribute name. This is required if either "label" or "value" is not specified.
* - label: the label associated with the attribute. If this is not specified, it will be generated from the attribute name.
* - value: the value to be displayed. If this is not specified, it will be retrieved from [[model]] using the attribute name
* by calling [[ArrayHelper::getValue()]]. Note that this value will be formatted into a displayable text
......@@ -176,10 +176,10 @@ class DetailView extends Widget
foreach ($this->attributes as $i => $attribute) {
if (is_string($attribute)) {
if (!preg_match('/^([\w\.]+)(:(\w*))?(:(.*))?$/', $attribute, $matches)) {
throw new InvalidConfigException('The attribute must be specified in the format of "name", "name:format" or "name:format:label"');
throw new InvalidConfigException('The attribute must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"');
}
$attribute = [
'name' => $matches[1],
'attribute' => $matches[1],
'format' => isset($matches[3]) ? $matches[3] : 'text',
'label' => isset($matches[5]) ? $matches[5] : null,
];
......@@ -197,16 +197,16 @@ class DetailView extends Widget
if (!isset($attribute['format'])) {
$attribute['format'] = 'text';
}
if (isset($attribute['name'])) {
$name = $attribute['name'];
if (isset($attribute['attribute'])) {
$attributeName = $attribute['attribute'];
if (!isset($attribute['label'])) {
$attribute['label'] = $this->model instanceof Model ? $this->model->getAttributeLabel($name) : Inflector::camel2words($name, true);
$attribute['label'] = $this->model instanceof Model ? $this->model->getAttributeLabel($attributeName) : Inflector::camel2words($attributeName, true);
}
if (!array_key_exists('value', $attribute)) {
$attribute['value'] = ArrayHelper::getValue($this->model, $name);
$attribute['value'] = ArrayHelper::getValue($this->model, $attributeName);
}
} elseif (!isset($attribute['label']) || !array_key_exists('value', $attribute)) {
throw new InvalidConfigException('The attribute configuration requires the "name" element to determine the value and display label.');
throw new InvalidConfigException('The attribute configuration requires the "attribute" element to determine the value and display label.');
}
$this->attributes[$i] = $attribute;
......
......@@ -102,8 +102,9 @@ class Pjax extends Widget
if ($view->title !== null) {
echo Html::tag('title', Html::encode($view->title));
}
} else {
echo Html::beginTag('div', $this->options);
}
echo Html::beginTag('div', $this->options);
}
/**
......@@ -111,9 +112,8 @@ class Pjax extends Widget
*/
public function run()
{
echo Html::endTag('div');
if (!$this->requiresPjax()) {
echo Html::endTag('div');
$this->registerClientScript();
return;
}
......@@ -152,7 +152,7 @@ class Pjax extends Widget
protected function requiresPjax()
{
$headers = Yii::$app->getRequest()->getHeaders();
return $headers->get('X-Pjax') && ($selector = $headers->get('X-Pjax-Container')) === '#' . $this->getId();
return $headers->get('X-Pjax') && $headers->get('X-Pjax-Container') === '#' . $this->getId();
}
/**
......
<?php
namespace yiiunit\data\ar;
use yii\db\ActiveQuery;
use yiiunit\framework\db\ActiveRecordTest;
/**
......@@ -11,6 +12,9 @@ use yiiunit\framework\db\ActiveRecordTest;
* @property string $email
* @property string $address
* @property integer $status
*
* @method CustomerQuery|Customer|null find($q = null) static
* @method CustomerQuery findBySql($sql, $params = []) static
*/
class Customer extends ActiveRecord
{
......@@ -24,6 +28,11 @@ class Customer extends ActiveRecord
return 'tbl_customer';
}
public function getProfile()
{
return $this->hasOne(Profile::className(), ['id' => 'profile_id']);
}
public function getOrders()
{
return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('id');
......@@ -34,6 +43,17 @@ class Customer extends ActiveRecord
return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer2')->orderBy('id');
}
// deeply nested table relation
public function getOrderItems()
{
/** @var ActiveQuery $rel */
$rel = $this->hasMany(Item::className(), ['id' => 'item_id']);
return $rel->viaTable('tbl_order_item', ['order_id' => 'id'], function($q) {
/** @var ActiveQuery $q */
$q->viaTable('tbl_order', ['customer_id' => 'id']);
})->orderBy('id');
}
public function afterSave($insert)
{
ActiveRecordTest::$afterSaveInsert = $insert;
......
<?php
/**
* @author Carsten Brandt <mail@cebe.cc>
*/
namespace yiiunit\data\ar;
/**
* Class Profile
*
* @property integer $id
* @property string $description
*
*/
class Profile extends ActiveRecord
{
public static function tableName()
{
return 'tbl_profile';
}
}
\ No newline at end of file
......@@ -13,7 +13,7 @@ class Customer extends ActiveRecord
public function attributes()
{
return ['id', 'email', 'name', 'address', 'status'];
return ['id', 'email', 'name', 'address', 'status', 'profile_id'];
}
/**
......@@ -36,4 +36,4 @@ class Customer extends ActiveRecord
$config['modelClass'] = get_called_class();
return new CustomerQuery($config);
}
}
\ No newline at end of file
}
......@@ -9,6 +9,7 @@ DROP TABLE IF EXISTS tbl_item;
DROP TABLE IF EXISTS tbl_order;
DROP TABLE IF EXISTS tbl_category;
DROP TABLE IF EXISTS tbl_customer;
DROP TABLE IF EXISTS tbl_profile;
DROP TABLE IF EXISTS tbl_null_values;
DROP TABLE IF EXISTS tbl_type;
DROP TABLE IF EXISTS tbl_constraints;
......@@ -20,12 +21,19 @@ CREATE TABLE `tbl_constraints`
);
CREATE TABLE `tbl_profile` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`description` varchar(128) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `tbl_customer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(128) NOT NULL,
`name` varchar(128),
`address` string,
`status` int (11) DEFAULT 0,
`profile_id` int(11),
PRIMARY KEY (`id`)
);
......@@ -94,9 +102,12 @@ CREATE TABLE `tbl_composite_fk` (
CONSTRAINT `FK_composite_fk_order_item` FOREIGN KEY (`order_id`,`item_id`) REFERENCES `tbl_order_item` (`order_id`,`item_id`) ON DELETE CASCADE
);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user1@example.com', 'user1', 'address1', 1);
INSERT INTO tbl_profile (description) VALUES ('profile customer 1');
INSERT INTO tbl_profile (description) VALUES ('profile customer 3');
INSERT INTO tbl_customer (email, name, address, status, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, 1);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user2@example.com', 'user2', 'address2', 1);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user3@example.com', 'user3', 'address3', 2);
INSERT INTO tbl_customer (email, name, address, status, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, 2);
INSERT INTO tbl_category (name) VALUES ('Books');
INSERT INTO tbl_category (name) VALUES ('Movies');
......
......@@ -3,15 +3,25 @@ IF OBJECT_ID('[dbo].[tbl_item]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_item];
IF OBJECT_ID('[dbo].[tbl_order]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_order];
IF OBJECT_ID('[dbo].[tbl_category]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_category];
IF OBJECT_ID('[dbo].[tbl_customer]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_customer];
IF OBJECT_ID('[dbo].[tbl_profile]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_profile];
IF OBJECT_ID('[dbo].[tbl_type]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_type];
IF OBJECT_ID('[dbo].[tbl_null_values]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_null_values];
CREATE TABLE [dbo].[tbl_profile] (
[id] [int] IDENTITY(1,1) NOT NULL,
[description] [varchar](128) NOT NULL,
CONSTRAINT [PK_customer] PRIMARY KEY CLUSTERED (
[id] ASC
) ON [PRIMARY]
);
CREATE TABLE [dbo].[tbl_customer] (
[id] [int] IDENTITY(1,1) NOT NULL,
[email] [varchar](128) NOT NULL,
[name] [varchar](128),
[address] [text],
[status] [int] DEFAULT 0,
[profile_id] [int],
CONSTRAINT [PK_customer] PRIMARY KEY CLUSTERED (
[id] ASC
) ON [PRIMARY]
......@@ -79,9 +89,12 @@ CREATE TABLE [dbo].[tbl_type] (
[bool_col2] [tinyint] DEFAULT '1'
);
INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status]) VALUES ('user1@example.com', 'user1', 'address1', 1);
INSERT INTO [dbo].[tbl_profile] ([description]) VALUES ('profile customer 1');
INSERT INTO [dbo].[tbl_profile] ([description]) VALUES ('profile customer 3');
INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status], [profile_id]) VALUES ('user1@example.com', 'user1', 'address1', 1, 1);
INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status]) VALUES ('user2@example.com', 'user2', 'address2', 1);
INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status]) VALUES ('user3@example.com', 'user3', 'address3', 2);
INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status], [profile_id]) VALUES ('user3@example.com', 'user3', 'address3', 2, 2);
INSERT INTO [dbo].[tbl_category] ([name]) VALUES ('Books');
INSERT INTO [dbo].[tbl_category] ([name]) VALUES ('Movies');
......
......@@ -9,6 +9,7 @@ DROP TABLE IF EXISTS tbl_item CASCADE;
DROP TABLE IF EXISTS tbl_order CASCADE;
DROP TABLE IF EXISTS tbl_category CASCADE;
DROP TABLE IF EXISTS tbl_customer CASCADE;
DROP TABLE IF EXISTS tbl_profile CASCADE;
DROP TABLE IF EXISTS tbl_null_values CASCADE;
DROP TABLE IF EXISTS tbl_type CASCADE;
DROP TABLE IF EXISTS tbl_constraints CASCADE;
......@@ -20,12 +21,19 @@ CREATE TABLE `tbl_constraints`
);
CREATE TABLE `tbl_profile` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`description` varchar(128) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `tbl_customer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(128) NOT NULL,
`name` varchar(128),
`address` text,
`status` int (11) DEFAULT 0,
`profile_id` int(11),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
......@@ -96,9 +104,12 @@ CREATE TABLE `tbl_type` (
`bool_col2` tinyint(1) DEFAULT '1'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user1@example.com', 'user1', 'address1', 1);
INSERT INTO tbl_profile (description) VALUES ('profile customer 1');
INSERT INTO tbl_profile (description) VALUES ('profile customer 3');
INSERT INTO tbl_customer (email, name, address, status, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, 1);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user2@example.com', 'user2', 'address2', 1);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user3@example.com', 'user3', 'address3', 2);
INSERT INTO tbl_customer (email, name, address, status, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, 2);
INSERT INTO tbl_category (name) VALUES ('Books');
INSERT INTO tbl_category (name) VALUES ('Movies');
......
......@@ -9,6 +9,7 @@ DROP TABLE IF EXISTS tbl_item CASCADE;
DROP TABLE IF EXISTS tbl_order CASCADE;
DROP TABLE IF EXISTS tbl_category CASCADE;
DROP TABLE IF EXISTS tbl_customer CASCADE;
DROP TABLE IF EXISTS tbl_profile CASCADE;
DROP TABLE IF EXISTS tbl_type CASCADE;
DROP TABLE IF EXISTS tbl_null_values CASCADE;
DROP TABLE IF EXISTS tbl_constraints CASCADE;
......@@ -19,12 +20,18 @@ CREATE TABLE tbl_constraints
field1 varchar(255)
);
CREATE TABLE tbl_profile (
id serial not null primary key,
description varchar(128) NOT NULL
);
CREATE TABLE tbl_customer (
id serial not null primary key,
email varchar(128) NOT NULL,
name varchar(128),
address text,
status integer DEFAULT 0
status integer DEFAULT 0,
profile_id integer
);
comment on column public.tbl_customer.email is 'someone@example.com';
......@@ -79,9 +86,12 @@ CREATE TABLE tbl_type (
bool_col2 smallint DEFAULT '1'
);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user1@example.com', 'user1', 'address1', 1);
INSERT INTO tbl_profile (description) VALUES ('profile customer 1');
INSERT INTO tbl_profile (description) VALUES ('profile customer 3');
INSERT INTO tbl_customer (email, name, address, status, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, 1);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user2@example.com', 'user2', 'address2', 1);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user3@example.com', 'user3', 'address3', 2);
INSERT INTO tbl_customer (email, name, address, status, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, 2);
INSERT INTO tbl_category (name) VALUES ('Books');
INSERT INTO tbl_category (name) VALUES ('Movies');
......
......@@ -9,15 +9,23 @@ DROP TABLE IF EXISTS tbl_item;
DROP TABLE IF EXISTS tbl_order;
DROP TABLE IF EXISTS tbl_category;
DROP TABLE IF EXISTS tbl_customer;
DROP TABLE IF EXISTS tbl_profile;
DROP TABLE IF EXISTS tbl_type;
DROP TABLE IF EXISTS tbl_null_values;
CREATE TABLE tbl_profile (
id INTEGER NOT NULL,
description varchar(128) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE tbl_customer (
id INTEGER NOT NULL,
email varchar(128) NOT NULL,
name varchar(128),
address text,
status INTEGER DEFAULT 0,
profile_id INTEGER,
PRIMARY KEY (id)
);
......@@ -81,9 +89,12 @@ CREATE TABLE tbl_type (
bool_col2 tinyint(1) DEFAULT '1'
);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user1@example.com', 'user1', 'address1', 1);
INSERT INTO tbl_profile (description) VALUES ('profile customer 1');
INSERT INTO tbl_profile (description) VALUES ('profile customer 3');
INSERT INTO tbl_customer (email, name, address, status, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, 1);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user2@example.com', 'user2', 'address2', 1);
INSERT INTO tbl_customer (email, name, address, status) VALUES ('user3@example.com', 'user3', 'address3', 2);
INSERT INTO tbl_customer (email, name, address, status, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, 2);
INSERT INTO tbl_category (name) VALUES ('Books');
INSERT INTO tbl_category (name) VALUES ('Movies');
......
......@@ -154,11 +154,12 @@ trait ActiveRecordTestTrait
// asArray
$customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one();
$this->assertEquals([
'id' => '2',
'id' => 2,
'email' => 'user2@example.com',
'name' => 'user2',
'address' => 'address2',
'status' => '1',
'status' => 1,
'profile_id' => null,
], $customer);
}
......
......@@ -7,6 +7,7 @@ use yiiunit\data\ar\NullValues;
use yiiunit\data\ar\OrderItem;
use yiiunit\data\ar\Order;
use yiiunit\data\ar\Item;
use yiiunit\data\ar\Profile;
use yiiunit\framework\ar\ActiveRecordTestTrait;
/**
......@@ -131,6 +132,22 @@ class ActiveRecordTest extends DatabaseTestCase
$this->assertEquals(2, $order['books'][1]['id']);
}
// deeply nested table relation
public function testDeeplyNestedTableRelation()
{
/** @var Customer $customer */
$customer = $this->callCustomerFind(1);
$this->assertNotNull($customer);
$items = $customer->orderItems;
$this->assertEquals(2, count($items));
$this->assertInstanceOf(Item::className(), $items[0]);
$this->assertInstanceOf(Item::className(), $items[1]);
$this->assertEquals(1, $items[0]->id);
$this->assertEquals(2, $items[1]->id);
}
public function testStoreNull()
{
$record = new NullValues();
......@@ -325,6 +342,34 @@ class ActiveRecordTest extends DatabaseTestCase
$this->assertEquals(1, count($orders[2]->books2));
}
public function testJoinWithAndScope()
{
// hasOne inner join
$customers = Customer::find()->active()->innerJoinWith('profile')->orderBy('tbl_customer.id')->all();
$this->assertEquals(1, count($customers));
$this->assertEquals(1, $customers[0]->id);
$this->assertTrue($customers[0]->isRelationPopulated('profile'));
// hasOne outer join
$customers = Customer::find()->active()->joinWith('profile')->orderBy('tbl_customer.id')->all();
$this->assertEquals(2, count($customers));
$this->assertEquals(1, $customers[0]->id);
$this->assertEquals(2, $customers[1]->id);
$this->assertTrue($customers[0]->isRelationPopulated('profile'));
$this->assertTrue($customers[1]->isRelationPopulated('profile'));
$this->assertInstanceOf(Profile::className(), $customers[0]->profile);
$this->assertNull($customers[1]->profile);
// hasMany
$customers = Customer::find()->active()->joinWith('orders')->orderBy('tbl_customer.id DESC, tbl_order.id')->all();
$this->assertEquals(2, count($customers));
$this->assertEquals(2, $customers[0]->id);
$this->assertEquals(1, $customers[1]->id);
$this->assertTrue($customers[0]->isRelationPopulated('orders'));
$this->assertTrue($customers[1]->isRelationPopulated('orders'));
}
public function testInverseOf()
{
// eager loading: find one and all
......
......@@ -536,6 +536,62 @@ EOD;
$this->assertEquals([], $options);
}
public function testCssStyleFromArray()
{
$this->assertEquals('width: 100px; height: 200px;', Html::cssStyleFromArray([
'width' => '100px',
'height' => '200px',
]));
$this->assertNull(Html::cssStyleFromArray([]));
}
public function testCssStyleToArray()
{
$this->assertEquals([
'width' => '100px',
'height' => '200px',
], Html::cssStyleToArray('width: 100px; height: 200px;'));
$this->assertEquals([], Html::cssStyleToArray(' '));
}
public function testAddCssStyle()
{
$options = ['style' => 'width: 100px; height: 200px;'];
Html::addCssStyle($options, 'width: 110px; color: red;');
$this->assertEquals('width: 110px; height: 200px; color: red;', $options['style']);
$options = ['style' => 'width: 100px; height: 200px;'];
Html::addCssStyle($options, ['width' => '110px', 'color' => 'red']);
$this->assertEquals('width: 110px; height: 200px; color: red;', $options['style']);
$options = ['style' => 'width: 100px; height: 200px;'];
Html::addCssStyle($options, 'width: 110px; color: red;', false);
$this->assertEquals('width: 100px; height: 200px; color: red;', $options['style']);
$options = [];
Html::addCssStyle($options, 'width: 110px; color: red;');
$this->assertEquals('width: 110px; color: red;', $options['style']);
$options = [];
Html::addCssStyle($options, 'width: 110px; color: red;', false);
$this->assertEquals('width: 110px; color: red;', $options['style']);
}
public function testRemoveCssStyle()
{
$options = ['style' => 'width: 110px; height: 200px; color: red;'];
Html::removeCssStyle($options, 'width');
$this->assertEquals('height: 200px; color: red;', $options['style']);
Html::removeCssStyle($options, ['height']);
$this->assertEquals('color: red;', $options['style']);
Html::removeCssStyle($options, ['color', 'background']);
$this->assertNull($options['style']);
$options = [];
Html::removeCssStyle($options, ['color', 'background']);
$this->assertTrue(!array_key_exists('style', $options));
}
protected function getDataItems()
{
return [
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\web;
use yii\web\Request;
use yiiunit\TestCase;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class RequestTest extends TestCase
{
public function testParseAcceptHeader()
{
$request = new Request;
$this->assertEquals([], $request->parseAcceptHeader(' '));
$this->assertEquals([
'audio/basic' => ['q' => 1],
'audio/*' => ['q' => 0.2],
], $request->parseAcceptHeader('audio/*; q=0.2, audio/basic'));
$this->assertEquals([
'application/json' => ['q' => 1, 'version' => '1.0'],
'application/xml' => ['q' => 1, 'version' => '2.0', 'x'],
'text/x-c' => ['q' => 1],
'text/x-dvi' => ['q' => 0.8],
'text/plain' => ['q' => 0.5],
], $request->parseAcceptHeader('text/plain; q=0.5,
application/json; version=1.0,
application/xml; version=2.0; x,
text/x-dvi; q=0.8, text/x-c'));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment