diff --git a/.travis.yml b/.travis.yml index 1a457a9..5ebd1b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,14 @@ php: - 5.4 - 5.5 - 5.6 -# - hhvm # commented until composer or hhvm get fixed: https://github.com/facebook/hhvm/issues/1347 + - hhvm + +# run build against PHP 5.6 and hhvm but allow them to fail +# http://docs.travis-ci.com/user/build-configuration/#Rows-That-are-Allowed-To-Fail +matrix: + allow_failures: + - php: hhvm + - php: 5.6 services: - redis-server diff --git a/apps/advanced/backend/controllers/SiteController.php b/apps/advanced/backend/controllers/SiteController.php index baa8dbe..60d3c1b 100644 --- a/apps/advanced/backend/controllers/SiteController.php +++ b/apps/advanced/backend/controllers/SiteController.php @@ -5,6 +5,7 @@ use Yii; use yii\web\AccessControl; use yii\web\Controller; use common\models\LoginForm; +use yii\web\VerbFilter; /** * Site controller @@ -31,6 +32,12 @@ class SiteController extends Controller ], ], ], + 'verbs' => [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'logout' => ['post'], + ], + ], ]; } diff --git a/apps/advanced/backend/views/layouts/main.php b/apps/advanced/backend/views/layouts/main.php index cb41c08..0a1bf13 100644 --- a/apps/advanced/backend/views/layouts/main.php +++ b/apps/advanced/backend/views/layouts/main.php @@ -37,7 +37,11 @@ AppAsset::register($this); if (Yii::$app->user->isGuest) { $menuItems[] = ['label' => 'Login', 'url' => ['/site/login']]; } else { - $menuItems[] = ['label' => 'Logout (' . Yii::$app->user->identity->username .')' , 'url' => ['/site/logout']]; + $menuItems[] = [ + 'label' => 'Logout (' . Yii::$app->user->identity->username . ')', + 'url' => ['/site/logout'], + 'linkOptions' => ['data-method' => 'post'] + ]; } echo Nav::widget([ 'options' => ['class' => 'navbar-nav navbar-right'], diff --git a/apps/advanced/common/mail/layouts/html.php b/apps/advanced/common/mail/layouts/html.php index 2e6b615..8e2707d 100644 --- a/apps/advanced/common/mail/layouts/html.php +++ b/apps/advanced/common/mail/layouts/html.php @@ -1,10 +1,9 @@ <?php use yii\helpers\Html; -use yii\mail\BaseMessage; /** * @var \yii\web\View $this - * @var BaseMessage $content + * @var \yii\mail\BaseMessage $content */ ?> <?php $this->beginPage() ?> diff --git a/apps/advanced/common/models/User.php b/apps/advanced/common/models/User.php index fc74532..d2c80c0 100644 --- a/apps/advanced/common/models/User.php +++ b/apps/advanced/common/models/User.php @@ -1,6 +1,7 @@ <?php namespace common\models; +use yii\base\NotSupportedException; use yii\db\ActiveRecord; use yii\helpers\Security; use yii\web\IdentityInterface; @@ -72,6 +73,14 @@ class User extends ActiveRecord implements IdentityInterface } /** + * @inheritdoc + */ + public static function findIdentityByAccessToken($token) + { + throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.'); + } + + /** * Finds user by username * * @param string $username diff --git a/apps/advanced/common/tests/_helpers/FixtureHelper.php b/apps/advanced/common/tests/_helpers/FixtureHelper.php index 00c3a17..507b60c 100644 --- a/apps/advanced/common/tests/_helpers/FixtureHelper.php +++ b/apps/advanced/common/tests/_helpers/FixtureHelper.php @@ -32,7 +32,7 @@ class FixtureHelper extends Module * to use in acceptance and functional tests. * @param array $settings */ - public function _beforeSuite($settings = array()) + public function _beforeSuite($settings = []) { $this->loadFixtures(); } @@ -54,5 +54,4 @@ class FixtureHelper extends Module ], ]; } - } diff --git a/apps/advanced/common/tests/_pages/LoginPage.php b/apps/advanced/common/tests/_pages/LoginPage.php index af95ea3..5fdf3bc 100644 --- a/apps/advanced/common/tests/_pages/LoginPage.php +++ b/apps/advanced/common/tests/_pages/LoginPage.php @@ -18,5 +18,4 @@ class LoginPage extends BasePage $this->guy->fillField('input[name="LoginForm[password]"]', $password); $this->guy->click('login-button'); } - } diff --git a/apps/advanced/common/tests/unit/models/LoginFormTest.php b/apps/advanced/common/tests/unit/models/LoginFormTest.php index 2ececd6..9ca277d 100644 --- a/apps/advanced/common/tests/unit/models/LoginFormTest.php +++ b/apps/advanced/common/tests/unit/models/LoginFormTest.php @@ -25,7 +25,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(); }); @@ -52,7 +52,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(); @@ -61,9 +61,8 @@ class LoginFormTest extends TestCase private function mockUser($user) { - $loginForm = $this->getMock('common\models\LoginForm',['getUser']); + $loginForm = $this->getMock('common\models\LoginForm', ['getUser']); $loginForm->expects($this->any())->method('getUser')->will($this->returnValue($user)); return $loginForm; } - } diff --git a/apps/advanced/composer.json b/apps/advanced/composer.json index 53e926f..7744971 100644 --- a/apps/advanced/composer.json +++ b/apps/advanced/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-app-advanced", "description": "Yii 2 Advanced Application Template", - "keywords": ["yii", "framework", "advanced", "application template"], + "keywords": ["yii2", "framework", "advanced", "application template"], "homepage": "http://www.yiiframework.com/", "type": "project", "license": "BSD-3-Clause", diff --git a/apps/advanced/frontend/controllers/SiteController.php b/apps/advanced/frontend/controllers/SiteController.php index 6816daf..a45c837 100644 --- a/apps/advanced/frontend/controllers/SiteController.php +++ b/apps/advanced/frontend/controllers/SiteController.php @@ -10,6 +10,7 @@ use yii\base\InvalidParamException; use yii\web\BadRequestHttpException; use yii\web\Controller; use Yii; +use yii\web\VerbFilter; /** * Site controller @@ -38,6 +39,12 @@ class SiteController extends Controller ], ], ], + 'verbs' => [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'logout' => ['post'], + ], + ], ]; } diff --git a/apps/advanced/frontend/tests/_pages/SignupPage.php b/apps/advanced/frontend/tests/_pages/SignupPage.php index ba4a9cb..0281ac9 100644 --- a/apps/advanced/frontend/tests/_pages/SignupPage.php +++ b/apps/advanced/frontend/tests/_pages/SignupPage.php @@ -20,5 +20,4 @@ class SignupPage extends BasePage } $this->guy->click('signup-button'); } - } diff --git a/apps/advanced/frontend/tests/acceptance/SignupCest.php b/apps/advanced/frontend/tests/acceptance/SignupCest.php index a166f34..268ff6b 100644 --- a/apps/advanced/frontend/tests/acceptance/SignupCest.php +++ b/apps/advanced/frontend/tests/acceptance/SignupCest.php @@ -79,5 +79,4 @@ class SignupCest $I->expectTo('see that user logged in'); $I->see('Logout (tester)'); } - } diff --git a/apps/advanced/frontend/tests/functional/SignupCest.php b/apps/advanced/frontend/tests/functional/SignupCest.php index cfef787..0409d23 100644 --- a/apps/advanced/frontend/tests/functional/SignupCest.php +++ b/apps/advanced/frontend/tests/functional/SignupCest.php @@ -79,5 +79,4 @@ class SignupCest $I->expectTo('see that user logged in'); $I->see('Logout (tester)'); } - } diff --git a/apps/advanced/frontend/tests/unit/models/ContactFormTest.php b/apps/advanced/frontend/tests/unit/models/ContactFormTest.php index 23e6288..a754672 100644 --- a/apps/advanced/frontend/tests/unit/models/ContactFormTest.php +++ b/apps/advanced/frontend/tests/unit/models/ContactFormTest.php @@ -42,7 +42,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 +56,4 @@ class ContactFormTest extends TestCase { return Yii::getAlias(Yii::$app->mail->fileTransportPath) . '/testing_message.eml'; } - } diff --git a/apps/advanced/frontend/tests/unit/models/PasswordResetRequestFormTest.php b/apps/advanced/frontend/tests/unit/models/PasswordResetRequestFormTest.php index b736039..7591ff3 100644 --- a/apps/advanced/frontend/tests/unit/models/PasswordResetRequestFormTest.php +++ b/apps/advanced/frontend/tests/unit/models/PasswordResetRequestFormTest.php @@ -28,14 +28,14 @@ class PasswordResetRequestFormTest extends DbTestCase public function testSendEmailWrongUser() { - $this->specify('no user with such email, message should not be send', function() { + $this->specify('no user with such email, message should not be send', function () { $model = new PasswordResetRequestForm(); $model->email = 'not-existing-email@example.com'; expect('email not send', $model->sendEmail())->false(); }); - $this->specify('user is not active, message should not be send', function() { + $this->specify('user is not active, message should not be send', function () { $model = new PasswordResetRequestForm(); $model->email = $this->user[1]['email']; @@ -52,8 +52,8 @@ class PasswordResetRequestFormTest extends DbTestCase expect('email sent', $model->sendEmail())->true(); expect('user has valid token', $user->password_reset_token)->notNull(); - $this->specify('message has correct format', function() use ($model) { - expect('message file exists', file_exists($this->getMessageFile()))->true(); + $this->specify('message has correct format', function () use ($model) { + expect('message file exists', file_exists($this->getMessageFile()))->true(); $message = file_get_contents($this->getMessageFile()); expect('message "from" is correct', $message)->contains(Yii::$app->params['supportEmail']); @@ -75,5 +75,4 @@ class PasswordResetRequestFormTest extends DbTestCase { return Yii::getAlias(Yii::$app->mail->fileTransportPath) . '/testing_message.eml'; } - } diff --git a/apps/advanced/frontend/tests/unit/models/ResetPasswordFormTest.php b/apps/advanced/frontend/tests/unit/models/ResetPasswordFormTest.php index 763683f..b00c2ac 100644 --- a/apps/advanced/frontend/tests/unit/models/ResetPasswordFormTest.php +++ b/apps/advanced/frontend/tests/unit/models/ResetPasswordFormTest.php @@ -13,13 +13,13 @@ class ResetPasswordFormTest extends DbTestCase public function testResetPassword() { - $this->specify('wrong reset token', function() { - $this->setExpectedException('\Exception','Wrong password reset token.'); + $this->specify('wrong reset token', function () { + $this->setExpectedException('\Exception', 'Wrong password reset token.'); new ResetPasswordForm('notexistingtoken_1391882543'); }); - $this->specify('not correct token', function() { - $this->setExpectedException('yii\base\InvalidParamException','Password reset token cannot be blank.'); + $this->specify('not correct token', function () { + $this->setExpectedException('yii\base\InvalidParamException', 'Password reset token cannot be blank.'); new ResetPasswordForm(''); }); } @@ -33,5 +33,4 @@ class ResetPasswordFormTest extends DbTestCase ], ]; } - } diff --git a/apps/advanced/frontend/tests/unit/models/SignupFormTest.php b/apps/advanced/frontend/tests/unit/models/SignupFormTest.php index 5176baa..359a02c 100644 --- a/apps/advanced/frontend/tests/unit/models/SignupFormTest.php +++ b/apps/advanced/frontend/tests/unit/models/SignupFormTest.php @@ -12,7 +12,7 @@ class SignupFormTest extends DbTestCase public function testCorrectSignup() { - $model = $this->getMock('frontend\models\SignupForm',['validate']); + $model = $this->getMock('frontend\models\SignupForm', ['validate']); $model->expects($this->once())->method('validate')->will($this->returnValue(true)); $model->username = 'some_username'; @@ -28,7 +28,7 @@ class SignupFormTest extends DbTestCase public function testNotCorrectSignup() { - $model = $this->getMock('frontend\models\SignupForm',['validate']); + $model = $this->getMock('frontend\models\SignupForm', ['validate']); $model->expects($this->once())->method('validate')->will($this->returnValue(false)); expect('user should not be created', $model->signup())->null(); @@ -43,5 +43,4 @@ class SignupFormTest extends DbTestCase ], ]; } - } diff --git a/apps/advanced/frontend/views/layouts/main.php b/apps/advanced/frontend/views/layouts/main.php index be8d70d..6e3ae8c 100644 --- a/apps/advanced/frontend/views/layouts/main.php +++ b/apps/advanced/frontend/views/layouts/main.php @@ -41,7 +41,11 @@ AppAsset::register($this); $menuItems[] = ['label' => 'Signup', 'url' => ['/site/signup']]; $menuItems[] = ['label' => 'Login', 'url' => ['/site/login']]; } else { - $menuItems[] = ['label' => 'Logout (' . Yii::$app->user->identity->username .')' , 'url' => ['/site/logout']]; + $menuItems[] = [ + 'label' => 'Logout (' . Yii::$app->user->identity->username . ')', + 'url' => ['/site/logout'], + 'linkOptions' => ['data-method' => 'post'] + ]; } echo Nav::widget([ 'options' => ['class' => 'navbar-nav navbar-right'], diff --git a/apps/basic/composer.json b/apps/basic/composer.json index 9990ef7..726a7ea 100644 --- a/apps/basic/composer.json +++ b/apps/basic/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-app-basic", "description": "Yii 2 Basic Application Template", - "keywords": ["yii", "framework", "basic", "application template"], + "keywords": ["yii2", "framework", "basic", "application template"], "homepage": "http://www.yiiframework.com/", "type": "project", "license": "BSD-3-Clause", diff --git a/apps/basic/mail/layouts/html.php b/apps/basic/mail/layouts/html.php index 2e6b615..8e2707d 100644 --- a/apps/basic/mail/layouts/html.php +++ b/apps/basic/mail/layouts/html.php @@ -1,10 +1,9 @@ <?php use yii\helpers\Html; -use yii\mail\BaseMessage; /** * @var \yii\web\View $this - * @var BaseMessage $content + * @var \yii\mail\BaseMessage $content */ ?> <?php $this->beginPage() ?> diff --git a/apps/basic/models/User.php b/apps/basic/models/User.php index b890e69..7a6fcc0 100644 --- a/apps/basic/models/User.php +++ b/apps/basic/models/User.php @@ -8,6 +8,7 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface public $username; public $password; public $authKey; + public $accessToken; private static $users = [ '100' => [ @@ -15,12 +16,14 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface 'username' => 'admin', 'password' => 'admin', 'authKey' => 'test100key', + 'accessToken' => '100-token', ], '101' => [ 'id' => '101', 'username' => 'demo', 'password' => 'demo', 'authKey' => 'test101key', + 'accessToken' => '101-token', ], ]; @@ -33,6 +36,19 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface } /** + * @inheritdoc + */ + public static function findIdentityByAccessToken($token) + { + foreach (self::$users as $user) { + if ($user['accessToken'] === $token) { + return new static($user); + } + } + return null; + } + + /** * Finds user by username * * @param string $username diff --git a/apps/basic/tests/_bootstrap.php b/apps/basic/tests/_bootstrap.php index 9340613..4890b3e 100644 --- a/apps/basic/tests/_bootstrap.php +++ b/apps/basic/tests/_bootstrap.php @@ -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__); diff --git a/apps/benchmark/composer.json b/apps/benchmark/composer.json index d980f9a..c8ed589 100644 --- a/apps/benchmark/composer.json +++ b/apps/benchmark/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-app-benchmark", "description": "Yii 2 Benchmark Application", - "keywords": ["yii", "framework", "benchmark", "application"], + "keywords": ["yii2", "framework", "benchmark", "application"], "homepage": "http://www.yiiframework.com/", "type": "project", "license": "BSD-3-Clause", diff --git a/build/controllers/ClassmapController.php b/build/controllers/ClassmapController.php index 706435a..5fd4e5f 100644 --- a/build/controllers/ClassmapController.php +++ b/build/controllers/ClassmapController.php @@ -52,7 +52,7 @@ class ClassmapController extends Controller $files = FileHelper::findFiles($root, $options); $map = []; foreach ($files as $file) { - if (($pos = strpos($file, $root)) !== 0) { + if (strpos($file, $root) !== 0) { die("Something wrong: $file\n"); } $path = str_replace('\\', '/', substr($file, strlen($root))); diff --git a/composer.json b/composer.json index d01de32..2466074 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-dev", "description": "Yii PHP Framework Version 2 - Development Package", - "keywords": ["yii", "framework"], + "keywords": ["yii2", "framework"], "homepage": "http://www.yiiframework.com/", "type": "yii2-extension", "license": "BSD-3-Clause", @@ -74,6 +74,7 @@ "lib-pcre": "*", "yiisoft/yii2-composer": "*", "yiisoft/jquery": "~2.0 | ~1.10", + "yiisoft/jquery-pjax": "*", "ezyang/htmlpurifier": "4.6.*", "cebe/markdown": "0.9.*" }, diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index c54bb08..6a3df60 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -172,7 +172,7 @@ Note that [[yii\db\ActiveRecord::updateAll()|updateAll()]], [[yii\db\ActiveRecor ```php // to insert a new customer record -$customer = new Customer; +$customer = new Customer(); $customer->name = 'James'; $customer->email = 'james@example.com'; $customer->save(); // equivalent to $customer->insert(); @@ -634,7 +634,7 @@ order owned by the customer: ```php $customer = Customer::find(1); -$order = new Order; +$order = new Order(); $order->subtotal = 100; $customer->link('orders', $order); ``` diff --git a/docs/guide/assets.md b/docs/guide/assets.md index 17c6797..ca3b07c 100644 --- a/docs/guide/assets.md +++ b/docs/guide/assets.md @@ -81,18 +81,26 @@ following way: ```php class LanguageAsset extends AssetBundle { - public $sourcePath = '@app/assets/language'; - public $js = [ - ]; - - public function init() - { - $this->js[] = 'language-' . Yii::$app->language . '.js'; - parent::init(); - } + public $language; + public $sourcePath = '@app/assets/language'; + public $js = [ + ]; + + public function registerAssetFiles($view) + { + $language = $this->language ? $this->language : Yii::$app->language; + $this->js[] = 'language-' . $language . '.js'; + parent::registerAssetFiles($view); + } } ``` +In order to set language use the following code when registering an asset bundle in a view: + +```php +LanguageAsset::register($this)->language = $language; +``` + Registering asset bundle ------------------------ @@ -114,6 +122,10 @@ To register an asset inside of a widget, the view instance is available as `$thi AppAsset::register($this->view); ``` +> Note: If there is a need to modify third party asset bundles it is recommended to create your own bundles depending + on third party ones and use CSS and JavaScript features to modify behavior instead of editing files directly or + copying them over. + Overriding asset bundles ------------------------ diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md index e7c1d94..f43ff0d 100644 --- a/docs/guide/authentication.md +++ b/docs/guide/authentication.md @@ -5,7 +5,7 @@ Authentication is the act of verifying who a user is, and is the basis of the lo In Yii, this entire process is performed semi-automatically, leaving the developer to merely implement [[yii\web\IdentityInterface]], the most important class in the authentication system. Typically, implementation of `IdentityInterface` is accomplished using the `User` model. -You can find a full featured example of authentication in the +You can find a fully featured example of authentication in the [advanced application template](installation.md). Below, only the interface methods are listed: ```php @@ -25,6 +25,17 @@ class User extends ActiveRecord implements IdentityInterface } /** + * Finds an identity by the given token. + * + * @param string $token the token to be looked for + * @return IdentityInterface|null the identity object that matches the given token. + */ + public static function findIdentityByAccessToken($token) + { + return static::find(['access_token' => $token]); + } + + /** * @return int|string current user ID */ public function getId() diff --git a/docs/guide/controller.md b/docs/guide/controller.md index 8bf03ad..eec7a89 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -81,13 +81,16 @@ Routes ------ Each controller action has a corresponding internal route. In our example above `actionIndex` has `site/index` route -and `actionTest` has `site/test` route. In this route `site` is referred to as controller ID while `test` is referred to -as action ID. +and `actionTest` has `site/test` route. In this route `site` is referred to as controller ID while `test` is action ID. By default you can access specific controller and action using the `http://example.com/?r=controller/action` URL. This -behavior is fully customizable. For details refer to [URL Management](url.md). +behavior is fully customizable. For more details please refer to [URL Management](url.md). -If controller is located inside a module its action internal route will be `module/controller/action`. +If a controller is located inside a module, the route of its actions will be in the format of `module/controller/action`. + +A controller can be located under a subdirectory of the controller directory of an application or module. The route +will be prefixed with the corresponding directory names. For example, you may have a `UserController` under `controllers/admin`. +The route of its `actionIndex` would be `admin/user/index`, and `admin/user` would be the controller ID. In case module, controller or action specified isn't found Yii will return "not found" page and HTTP status code 404. @@ -160,7 +163,7 @@ class BlogController extends Controller { $post = Post::find($id); if (!$post) { - throw new NotFoundHttpException; + throw new NotFoundHttpException(); } if (\Yii::$app->request->isPost) { diff --git a/docs/guide/data-grid.md b/docs/guide/data-grid.md new file mode 100644 index 0000000..87bfce6 --- /dev/null +++ b/docs/guide/data-grid.md @@ -0,0 +1,80 @@ +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 diff --git a/docs/guide/data-overview.md b/docs/guide/data-overview.md new file mode 100644 index 0000000..6318d18 --- /dev/null +++ b/docs/guide/data-overview.md @@ -0,0 +1,14 @@ +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 diff --git a/docs/guide/data-providers.md b/docs/guide/data-providers.md new file mode 100644 index 0000000..9080c55 --- /dev/null +++ b/docs/guide/data-providers.md @@ -0,0 +1,130 @@ +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 +------------------------------------------ + diff --git a/docs/guide/data-widgets.md b/docs/guide/data-widgets.md new file mode 100644 index 0000000..8046e8a --- /dev/null +++ b/docs/guide/data-widgets.md @@ -0,0 +1,34 @@ +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, + ], + ], +]); +``` diff --git a/docs/guide/index.md b/docs/guide/index.md index 349caa5..3d5305f 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -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 --------------- diff --git a/docs/guide/model.md b/docs/guide/model.md index f840457..6585c67 100644 --- a/docs/guide/model.md +++ b/docs/guide/model.md @@ -24,7 +24,7 @@ be accessed like the member variables of any object. For example, a `Post` model may contain a `title` attribute and a `content` attribute, accessible as follows: ```php -$post = new Post; +$post = new Post(); $post->title = 'Hello, world'; $post->content = 'Something interesting is happening.'; echo $post->title; @@ -35,7 +35,7 @@ Since [[yii\base\Model|Model]] implements the [ArrayAccess](http://php.net/manua you can also access the attributes as if they were array elements: ```php -$post = new Post; +$post = new Post(); $post['title'] = 'Hello, world'; $post['content'] = 'Something interesting is happening'; echo $post['title']; @@ -160,7 +160,7 @@ class EmployeeController extends \yii\web\Controller $employee = new Employee(['scenario' => 'managementPanel']); // second way - $employee = new Employee; + $employee = new Employee(); $employee->scenario = 'managementPanel'; // third way @@ -187,7 +187,7 @@ only, etc. If errors are found in validation, they may be presented to the user The following example shows how the validation is performed: ```php -$model = new LoginForm; +$model = new LoginForm(); $model->username = $_POST['username']; $model->password = $_POST['password']; if ($model->validate()) { @@ -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: diff --git a/docs/guide/module-debug.md b/docs/guide/module-debug.md index 91da33a..916101c 100644 --- a/docs/guide/module-debug.md +++ b/docs/guide/module-debug.md @@ -110,7 +110,7 @@ class ViewsPanel extends Panel { parent::init(); Event::on(View::className(), View::EVENT_BEFORE_RENDER, function (ViewEvent $event) { - $this->_viewFiles[] = $event->viewFile; + $this->_viewFiles[] = $event->sender->getViewFile(); }); } diff --git a/docs/guide/query-builder.md b/docs/guide/query-builder.md index c238639..a268311 100644 --- a/docs/guide/query-builder.md +++ b/docs/guide/query-builder.md @@ -9,7 +9,7 @@ The Query Builder provides an object-oriented vehicle for generating queries to A typical usage of the query builder looks like the following: ```php -$rows = (new \yii\db\Query) +$rows = (new \yii\db\Query()) ->select('id, name') ->from('tbl_user') ->limit(10) @@ -17,7 +17,7 @@ $rows = (new \yii\db\Query) // which is equivalent to the following code: -$query = (new \yii\db\Query) +$query = (new \yii\db\Query()) ->select('id, name') ->from('tbl_user') ->limit(10); @@ -116,7 +116,7 @@ You may specify a sub-query using a `Query` object. In this case, the correspond as the alias for the sub-query. ```php -$subQuery = (new Query)->select('id')->from('tbl_user')->where('status=1'); +$subQuery = (new Query())->select('id')->from('tbl_user')->where('status=1'); $query->select('*')->from(['u' => $subQuery]); ``` @@ -324,10 +324,10 @@ $query->leftJoin(['u' => $subQuery], 'u.id=author_id'); In Yii in order to build it you can first form two query objects and then use `union` method: ```php -$query = new Query; +$query = new Query(); $query->select("id, 'post' as type, name")->from('tbl_post')->limit(10); -$anotherQuery = new Query; +$anotherQuery = new Query(); $anotherQuery->select('id, 'user' as type, name')->from('tbl_user')->limit(10); $query->union($anotherQuery); @@ -347,7 +347,7 @@ Batch query can be used like the following: ```php use yii\db\Query; -$query = (new Query) +$query = (new Query()) ->from('tbl_user') ->orderBy('id'); @@ -376,7 +376,7 @@ will still keep the proper index. For example, ```php use yii\db\Query; -$query = (new Query) +$query = (new Query()) ->from('tbl_user') ->indexBy('username'); diff --git a/docs/guide/rest.md b/docs/guide/rest.md new file mode 100644 index 0000000..6fd215f --- /dev/null +++ b/docs/guide/rest.md @@ -0,0 +1,878 @@ +Implementing RESTful Web Service APIs +===================================== + +Yii provides a whole set of tools to greatly simplify the task of implementing RESTful Web Service APIs. +In particular, Yii provides support for the following aspects regarding RESTful APIs: + +* Quick prototyping with support for common APIs for ActiveRecord; +* Response format (supporting JSON and XML by default) and API version negotiation; +* Customizable object serialization with support for selectable output fields; +* Proper formatting of collection data and validation errors; +* Efficient routing with proper HTTP verb check; +* Support `OPTIONS` and `HEAD` verbs; +* Authentication; +* Authorization; +* Support for HATEOAS; +* Caching via `yii\web\HttpCache`; +* Rate limiting; +* Searching and filtering: TBD +* Testing: TBD +* Automatic generation of API documentation: TBD + + +A Quick Example +--------------- + +Let's use a quick example to show how to build a set of RESTful APIs using Yii. +Assume you want to expose the user data via RESTful APIs. The user data are stored in the user DB table, +and you have already created the ActiveRecord class `app\models\User` to access the user data. + +First, create a controller class `app\controllers\UserController` as follows, + +```php +namespace app\controllers; + +use yii\rest\ActiveController; + +class UserController extends ActiveController +{ + public $modelClass = 'app\models\User'; +} +``` + +Then, modify the configuration about the `urlManager` component in your application configuration: + +```php +'urlManager' => [ + 'enablePrettyUrl' => true, + 'enableStrictParsing' => true, + 'showScriptName' => false, + 'rules' => [ + ['class' => 'yii\rest\UrlRule', 'controller' => 'user'], + ], +] +``` + +With the above minimal amount of effort, you have already finished your task of creating the RESTful APIs +for accessing the user data. The APIs you have created include: + +* `GET /users`: list all users page by page; +* `HEAD /users`: show the overview information of user listing; +* `POST /users`: create a new user; +* `GET /users/123`: return the details of the user 123; +* `HEAD /users/123`: show the overview information of user 123; +* `PATCH /users/123` and `PUT /users/123`: update the user 123; +* `DELETE /users/123`: delete the user 123; +* `OPTIONS /users`: show the supported verbs regarding endpoint `/users`; +* `OPTIONS /users/123`: show the supported verbs regarding endpoint `/users/123`. + +You may access your APIs with the `curl` command like the following, + +``` +curl -i -H "Accept:application/json" "http://localhost/users" +``` + +which may give the following output: + +``` +HTTP/1.1 200 OK +Date: Sun, 02 Mar 2014 05:31:43 GMT +Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y +X-Powered-By: PHP/5.4.20 +X-Pagination-Total-Count: 1000 +X-Pagination-Page-Count: 50 +X-Pagination-Current-Page: 1 +X-Pagination-Per-Page: 20 +Link: <http://localhost/users?page=1>; rel=self, + <http://localhost/users?page=2>; rel=next, + <http://localhost/users?page=50>; rel=last +Transfer-Encoding: chunked +Content-Type: application/json; charset=UTF-8 + +[ + { + "id": 1, + ... + }, + { + "id": 2, + ... + }, + ... +] +``` + +Try changing the acceptable content type to be `application/xml`, and you will see the result +is returned in XML format: + +``` +curl -i -H "Accept:application/xml" "http://localhost/users" +``` + +``` +HTTP/1.1 200 OK +Date: Sun, 02 Mar 2014 05:31:43 GMT +Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y +X-Powered-By: PHP/5.4.20 +X-Pagination-Total-Count: 1000 +X-Pagination-Page-Count: 50 +X-Pagination-Current-Page: 1 +X-Pagination-Per-Page: 20 +Link: <http://localhost/users?page=1>; rel=self, + <http://localhost/users?page=2>; rel=next, + <http://localhost/users?page=50>; rel=last +Transfer-Encoding: chunked +Content-Type: application/xml + +<?xml version="1.0" encoding="UTF-8"?> +<response> + <item> + <id>1</id> + ... + </item> + <item> + <id>2</id> + ... + </item> + ... +</response> +``` + +> Tip: You may also access your APIs via Web browser by entering the URL `http://localhost/users`. + +As you can see, in the response headers, there are information about the total count, page count, etc. +There are also links that allow you to navigate to other pages of data. For example, `http://localhost/users?page=2` +would give you the next page of the user data. + +Using the `fields` and `expand` parameters, you may also request to return a subset of the fields in the result. +For example, the URL `http://localhost/users?fields=id,email` will only return the `id` and `email` fields in the result: + + +> Info: You may have noticed that the result of `http://localhost/users` includes some sensitive fields, +> such as `password_hash`, `auth_key`. You certainly do not want these to appear in your API result. +> You can/should filter out these fields as described in the following sections. + + +In the following sections, we will explain in more details about implementing RESTful APIs. + + +General Architecture +-------------------- + +Using the Yii RESTful API framework, you implement an API endpoint in terms of a controller action, and you use +a controller to organize the actions that implement the endpoints for a single type of resource. + +Resources are represented as data models which extend from the [[yii\base\Model]] class. +If you are working with databases (relational or NoSQL), it is recommended you use ActiveRecord to represent resources. + +You may use [[yii\rest\UrlRule]] to simplify the routing to your API endpoints. + +While not required, it is recommended that you develop your RESTful APIs as an application, separated from +your Web front end and back end. + + +Creating Resource Classes +------------------------- + +RESTful APIs are all about accessing and manipulating resources. In Yii, a resource can be an object of any class. +However, if your resource classes extend from [[yii\base\Model]] or its child classes (e.g. [[yii\db\ActiveRecord]]), +you may enjoy the following benefits: + +* Input data validation; +* Query, create, update and delete data, if extending from [[yii\db\ActiveRecord]]; +* Customizable data formatting (to be explained in the next section). + + +Formatting Response Data +------------------------ + +By default, Yii supports two response formats for RESTful APIs: JSON and XML. If you want to support +other formats, you should configure [[yii\rest\Controller::supportedFormats]] and also [[yii\web\Response::formatters]]. + +Formatting response data in general involves two steps: + +1. The objects (including embedded objects) in the response data are converted into arrays by [[yii\rest\Serializer]]; +2. The array data are converted into different formats (e.g. JSON, XML) by [[yii\web\ResponseFormatterInterface|response formatters]]. + +Step 2 is usually a very mechanical data conversion process and can be well handled by the built-in response formatters. +Step 1 involves some major development effort as explained below. + +When the [[yii\rest\Serializer|serializer]] converts an object into an array, it will call the `toArray()` method +of the object if it implements [[yii\base\ArrayableInterface]]. If an object does not implement this interface, +its public properties will be returned instead. + +For classes extending from [[yii\base\Model]] or [[yii\db\ActiveRecord]], besides directly overriding `toArray()`, +you may also override the `fields()` method and/or the `extraFields()` method to customize the data being returned. + +The method [[yii\base\Model::fields()]] declares a set of *fields* that should be included in the result. +A field is simply a named data item. In a result array, the array keys are the field names, and the array values +are the corresponding field values. The default implementation of [[yii\base\Model::fields()]] is to return +all attributes of a model as the output fields; for [[yii\db\ActiveRecord::fields()]], by default it will return +the names of the attributes whose values have been populated into the object. + +You can override the `fields()` method to add, remove, rename or redefine fields. For example, + +```php +// explicitly list every field, best used when you want to make sure the changes +// in your DB table or model attributes do not cause your field changes (to keep API backward compatibility). +public function fields() +{ + return [ + // field name is the same as the attribute name + 'id', + // field name is "email", the corresponding attribute name is "email_address" + 'email' => 'email_address', + // field name is "name", its value is defined by a PHP callback + 'name' => function () { + return $this->first_name . ' ' . $this->last_name; + }, + ]; +} + +// filter out some fields, best used when you want to inherit the parent implementation +// and blacklist some sensitive fields. +public function fields() +{ + $fields = parent::fields(); + + // remove fields that contain sensitive information + unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']); + + return $fields; +} +``` + +The return value of `fields()` should be an array. The array keys are the field names, and the array values +are the corresponding field definitions which can be either property/attribute names or anonymous functions +returning the corresponding field values. + +> Warning: Because by default all attributes of a model will be included in the API result, you should +> examine your data to make sure they do not contain sensitive information. If there is such information, +> you should override `fields()` or `toArray()` to filter them out. In the above example, we choose +> to filter out `auth_key`, `password_hash` and `password_reset_token`. + +You may use the `fields` query parameter to specify which fields in `fields()` should be included in the result. +If this parameter is not specified, all fields returned by `fields()` will be returned. + +The method [[yii\base\Model::extraFields()]] is very similar to [[yii\base\Model::fields()]]. +The difference between these methods is that the latter declares the fields that should be returned by default, +while the former declares the fields that should only be returned when the user specifies them in the `expand` query parameter. + +For example, `http://localhost/users?fields=id,email&expand=profile` may return the following JSON data: + +```php +[ + { + "id": 100, + "email": "100@example.com", + "profile": { + "id": 100, + "age": 30, + } + }, + ... +] +``` + +You may wonder who triggers the conversion from objects to arrays when an action returns an object or object collection. +The answer is that this is done by [[yii\rest\Controller::serializer]] in the [[yii\base\Controller::afterAction()|afterAction()]] +method. By default, [[yii\rest\Serializer]] is used as the serializer that can recognize resource objects extending from +[[yii\base\Model]] and collection objects implementing [[yii\data\DataProviderInterface]]. The serializer +will call the `toArray()` method of these objects and pass the `fields` and `expand` user parameters to the method. +If there are any embedded objects, they will also be converted into arrays recursively. + +If all your resource objects are of [[yii\base\Model]] or its child classes, such as [[yii\db\ActiveRecord]], +and you only use [[yii\data\DataProviderInterface]] as resource collections, the default data formatting +implementation should work very well. However, if you want to introduce some new resource classes that do not +extend from [[yii\base\Model]], or if you want to use some new collection classes, you will need to +customize the serializer class and configure [[yii\rest\Controller::serializer]] to use it. +You new resource classes may use the trait [[yii\base\ArrayableTrait]] to support selective field output +as explained above. + + +### Pagination + +For API endpoints about resource collections, pagination is supported out-of-box if you use +[[yii\data\DataProviderInterface|data provider]] to serve the response data. In particular, +through query parameters `page` and `per-page`, an API consumer may specify which page of data +to return and how many data items should be included in each page. The corresponding response +will include the pagination information by the following HTTP headers (please also refer to the first example +in this chapter): + +* `X-Pagination-Total-Count`: The total number of data items; +* `X-Pagination-Page-Count`: The number of pages; +* `X-Pagination-Current-Page`: The current page (1-based); +* `X-Pagination-Per-Page`: The number of data items in each page; +* `Link`: A set of navigational links allowing client to traverse the data page by page. + +The response body will contain a list of data items in the requested page. + +Sometimes, you may want to help simplify the client development work by including pagination information +directly in the response body. To do so, configure the [[yii\rest\Serializer::collectionEnvelope]] property +as follows: + +```php +use yii\rest\ActiveController; + +class UserController extends ActiveController +{ + public $modelClass = 'app\models\User'; + public $serializer = [ + 'class' => 'yii\rest\Serializer', + 'collectionEnvelope' => 'items', + ]; +} +``` + +You may then get the following response for request `http://localhost/users`: + +``` +HTTP/1.1 200 OK +Date: Sun, 02 Mar 2014 05:31:43 GMT +Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y +X-Powered-By: PHP/5.4.20 +X-Pagination-Total-Count: 1000 +X-Pagination-Page-Count: 50 +X-Pagination-Current-Page: 1 +X-Pagination-Per-Page: 20 +Link: <http://localhost/users?page=1>; rel=self, + <http://localhost/users?page=2>; rel=next, + <http://localhost/users?page=50>; rel=last +Transfer-Encoding: chunked +Content-Type: application/json; charset=UTF-8 + +{ + "items": [ + { + "id": 1, + ... + }, + { + "id": 2, + ... + }, + ... + ], + "_links": { + "self": "http://localhost/users?page=1", + "next": "http://localhost/users?page=2", + "last": "http://localhost/users?page=50" + }, + "_meta": { + "totalCount": 1000, + "pageCount": 50, + "currentPage": 1, + "perPage": 20 + } +} +``` + + +### HATEOAS Support + +[HATEOAS](http://en.wikipedia.org/wiki/HATEOAS), an abbreviation for Hypermedia as the Engine of Application State, +promotes that RESTful APIs should return information that allow clients to discover actions supported for the returned +resources. The key of HATEOAS is to return a set of hyperlinks with relation information when resource data are served +by APIs. + +You may let your model classes to implement the [[yii\web\Linkable]] interface to support HATEOAS. By implementing +this interface, a class is required to return a list of [[yii\web\Link|links]]. Typically, you should return at least +the `self` link, for example: + +```php +use yii\db\ActiveRecord; +use yii\web\Linkable; +use yii\helpers\Url; + +class User extends ActiveRecord implements Linkable +{ + public function getLinks() + { + return [ + Link::REL_SELF => Url::action(['user', 'id' => $this->id], true), + ]; + } +} +``` + +When a `User` object is returned in a response, it will contain a `_links` element representing the links related +to the user, for example, + +``` +{ + "id": 100, + "email": "user@example.com", + ..., + "_links" => [ + "self": "https://example.com/users/100" + ] +} +``` + + +Creating Controllers and Actions +-------------------------------- + +So you have the resource data and you have specified how the resource data should be formatted, the next thing +to do is to create controller actions to expose the resource data to end users. + +Yii provides two base controller classes to simplify your work of creating RESTful actions: +[[yii\rest\Controller]] and [[yii\rest\ActiveController]]. The difference between these two controllers +is that the latter provides a default set of actions that are specified designed to deal with +resources represented as ActiveRecord. So if you are using ActiveRecord and you are comfortable with +the provided built-in actions, you may consider creating your controller class by extending from +the latter. Otherwise, extending from [[yii\rest\Controller]] will allow you to develop actions +from scratch. + +Both [[yii\rest\Controller]] and [[yii\rest\ActiveController]] provide the following features which will +be described in detail in the next few sections: + +* Response format negotiation; +* API version negotiation; +* HTTP method validation; +* User authentication; +* Rate limiting. + +[[yii\rest\ActiveController]] in addition provides the following features specifically for working +with ActiveRecord: + +* A set of commonly used actions: `index`, `view`, `create`, `update`, `delete`, `options`; +* User authorization in regard to the requested action and resource. + +When creating a new controller class, a convention in naming the controller class is to use +the type name of the resource and use singular form. For example, to serve user information, +the controller may be named as `UserController`. + +Creating a new action is similar to creating an action for a Web application. The only difference +is that instead of rendering the result using a view by calling the `render()` method, for RESTful actions +you directly return the data. The [[yii\rest\Controller::serializer|serializer]] and the +[[yii\web\Response|response object]] will handle the conversion from the original data to the requested +format. For example, + +```php +public function actionSearch($keyword) +{ + $result = SolrService::search($keyword); + return $result; +} +``` + +If your controller class extends from [[yii\rest\ActiveController]], you should set +its [[yii\rest\ActiveController::modelClass||modelClass]] property to be the name of the resource class +that you plan to serve through this controller. The class must implement [[yii\db\ActiveRecordInterface]]. + +With [[yii\rest\ActiveController]], you may want to disable some of the built-in actions or customize them. +To do so, override the `actions()` method like the following: + +```php +public function actions() +{ + $actions = parent::actions(); + + // disable the "delete" and "create" actions + unset($actions['delete'], $actions['create']); + + // customize the data provider preparation with the "prepareDataProvider()" method + $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider']; + + return $actions; +} + +public function prepareDataProvider() +{ + // prepare and return a data provider for the "index" action +} +``` + +The following list summarizes the built-in actions supported by [[yii\rest\ActiveController]]: + +* [[yii\rest\IndexAction|index]]: list resources page by page; +* [[yii\rest\ViewAction|view]]: return the details of a specified resource; +* [[yii\rest\CreateAction|create]]: create a new resource; +* [[yii\rest\UpdateAction|update]]: update an existing resource; +* [[yii\rest\DeleteAction|delete]]: delete the specified resource; +* [[yii\rest\OptionsAction|options]]: return the supported HTTP methods. + + +Routing +------- + +With resource and controller classes ready, you can access the resources using the URL like +`http://localhost/index.php?r=user/create`. As you can see, the format of the URL is the same as that +for Web applications. + +In practice, you usually want to enable pretty URLs and take advantage of HTTP verbs. +For example, a request `POST /users` would mean accessing the `user/create` action. +This can be done easily by configuring the `urlManager` application component in the application +configuration like the following: + +```php +'urlManager' => [ + 'enablePrettyUrl' => true, + 'enableStrictParsing' => true, + 'showScriptName' => false, + 'rules' => [ + ['class' => 'yii\rest\UrlRule', 'controller' => 'user'], + ], +] +``` + +Compared to the URL management for Web applications, the main new thing above is the use of +[[yii\rest\UrlRule]] for routing RESTful API requests. This special URL rule class will +create a whole set of child URL rules to support routing and URL creation for the specified controller(s). +For example, the above code is roughly equivalent to the following rules: + +```php +[ + 'PUT,PATCH users/<id>' => 'user/update', + 'DELETE users/<id>' => 'user/delete', + 'GET,HEAD users/<id>' => 'user/view', + 'POST users' => 'user/create', + 'GET,HEAD users' => 'user/index', + 'users/<id>' => 'user/options', + 'users' => 'user/options', +] +``` + +And the following API endpoints are supported by this rule: + +* `GET /users`: list all users page by page; +* `HEAD /users`: show the overview information of user listing; +* `POST /users`: create a new user; +* `GET /users/123`: return the details of the user 123; +* `HEAD /users/123`: show the overview information of user 123; +* `PATCH /users/123` and `PUT /users/123`: update the user 123; +* `DELETE /users/123`: delete the user 123; +* `OPTIONS /users`: show the supported verbs regarding endpoint `/users`; +* `OPTIONS /users/123`: show the supported verbs regarding endpoint `/users/123`. + +You may configure the `only` and `except` options to explicitly list which actions to support or which +actions should be disabled, respectively. For example, + +```php +[ + 'class' => 'yii\rest\UrlRule', + 'controller' => 'user', + 'except' => ['delete', 'create', 'update'], +], +``` + +You may also configure `patterns` or `extra` to redefine existing patterns or add new patterns supported by this rule. +For example, to support a new action `search` by the endpoint `GET /users/search`, configure the `extra` option as follows, + +```php +[ + 'class' => 'yii\rest\UrlRule', + 'controller' => 'user', + 'extra' => [ + 'GET search' => 'search', + ], +``` + +You may have noticed that the controller ID `user` appears in plural form as `users` in the endpoints. +This is because [[yii\rest\UrlRule]] automatically pluralizes controller IDs for them to use in endpoints. +You may disable this behavior by setting [[yii\rest\UrlRule::pluralize]] to be false, or if you want +to use some special names you may configure the [[yii\rest\UrlRule::controller]] property. + + +Authentication +-------------- + +Unlike Web applications, RESTful APIs should be stateless, which means sessions or cookies should not +be used. Therefore, each request should come with some sort of authentication credentials because +the user authentication status may not be maintained by sessions or cookies. A common practice is +to send a secret access token with each request to authenticate the user. Since an access token +can be used to uniquely identify and authenticate a user, **the API requests should always be sent +via HTTPS to prevent from man-in-the-middle (MitM) attacks**. + +There are different ways to send an access token: + +* [HTTP Basic Auth](http://en.wikipedia.org/wiki/Basic_access_authentication): the access token + is sent as the username. This is should only be used when an access token can be safely stored + on the API consumer side. For example, the API consumer is a program running on a server. +* Query parameter: the access token is sent as a query parameter in the API URL, e.g., + `https://example.com/users?access-token=xxxxxxxx`. Because most Web servers will keep query + parameters in server logs, this approach should be mainly used to serve `JSONP` requests which + cannot use HTTP headers to send access tokens. +* [OAuth 2](http://oauth.net/2/): the access token is obtained by the consumer from an authorization + server and sent to the API server via [HTTP Bearer Tokens](http://tools.ietf.org/html/rfc6750), + according to the OAuth2 protocol. + +Yii supports all of the above authentication methods and can be further extended to support other methods. + +To enable authentication for your APIs, do the following two steps: + +1. Configure [[yii\rest\Controller::authMethods]] with the authentication methods you plan to use. +2. Implement [[yii\web\IdentityInterface::findIdentityByAccessToken()]] in your [[yii\web\User::identityClass|user identity class]]. + +For example, to enable all three authentication methods explained above, you would configure `authMethods` +as follows, + +```php +class UserController extends ActiveController +{ + public $authMethods = [ + 'yii\rest\HttpBasicAuth', + 'yii\rest\QueryParamAuth', + 'yii\rest\HttpBearerAuth', + ]; +} +``` + +Each element in `authMethods` should be an auth class name or a configuration array. An auth class +must implement [[yii\rest\AuthInterface]]. + +Implementation of `findIdentityByAccessToken()` is application specific. For example, in simple scenarios +when each user can only have one access token, you may store the access token in an `access_token` column +in the user table. The method can then be readily implemented in the `User` class as follows, + +```php +use yii\db\ActiveRecord; +use yii\web\IdentityInterface; + +class User extends ActiveRecord implements IdentityInterface +{ + public static function findIdentityByAccessToken($token) + { + return static::find(['access_token' => $token]); + } +} +``` + +After authentication is enabled as described above, for every API request, the requested controller +will try to authenticate the user in its `beforeAction()` step. + +If authentication succeeds, the controller will perform other checks (such as rate limiting, authorization) +and then run the action. The authenticated user identity information can be retrieved via `Yii::$app->user->identity`. + +If authentication fails, a response with HTTP status 401 will be sent back together with other appropriate headers +(such as a `WWW-Authenticate` header for HTTP Basic Auth). + + +Authorization +------------- + +After a user is authenticated, you probably want to check if he has the permission to perform the requested +action for the requested resource. This process is called *authorization* which is covered in detail in +the [Authorization chapter](authorization.md). + +You may use the [[yii\web\AccessControl]] filter and/or the Role-Based Access Control (RBAC) component +to implementation authorization. + +To simplify the authorization check, you may also override the [[yii\rest\Controller::checkAccess()]] method +and then call this method in places where authorization is needed. By default, the built-in actions provided +by [[yii\rest\ActiveController]] will call this method when they are about to run. + +```php +/** + * Checks the privilege of the current user. + * + * This method should be overridden to check whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param \yii\base\Model $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws ForbiddenHttpException if the user does not have access + */ +public function checkAccess($action, $model = null, $params = []) +{ +} +``` + + +Rate Limiting +------------- + +To prevent abuse, you should consider adding rate limiting to your APIs. For example, you may limit the API usage +of each user to be at most 100 API calls within a period of 10 minutes. If too many requests are received from a user +within the period of the time, a response with status code 429 (meaning Too Many Requests) should be returned. + +To enable rate limiting, the [[yii\web\User::identityClass|user identity class]] should implement [[yii\rest\RateLimitInterface]]. +This interface requires implementation of the following three methods: + +* `getRateLimit()`: returns the maximum number of allowed requests and the time period, e.g., `[100, 600]` means + at most 100 API calls within 600 seconds. +* `loadAllowance()`: returns the number of remaining requests allowed and the corresponding UNIX timestamp + when the rate limit is checked last time. +* `saveAllowance()`: saves the number of remaining requests allowed and the current UNIX timestamp. + +You may use two columns in the user table to record the allowance and timestamp information. +And `loadAllowance()` and `saveAllowance()` can then be implementation by reading and saving the values +of the two columns corresponding to the current authenticated user. To improve performance, you may also +consider storing these information in cache or some NoSQL storage. + +Once the identity class implements the required interface, Yii will automatically use the rate limiter +as specified by [[yii\rest\Controller::rateLimiter]] to perform rate limiting check. The rate limiter +will thrown a [[yii\web\TooManyRequestsHttpException]] if rate limit is exceeded. + +When rate limiting is enabled, every response will be sent with the following HTTP headers containing +the current rate limiting information: + +* `X-Rate-Limit-Limit`: The maximum number of requests allowed with a time period; +* `X-Rate-Limit-Remaining`: The number of remaining requests in the current time period; +* `X-Rate-Limit-Reset`: The number of seconds to wait in order to get the maximum number of allowed requests. + + +Error Handling +-------------- + +When handling a RESTful API request, if there is an error in the user request or if something unexpected +happens on the server, you may simply throw an exception to notify the user something wrong happened. +If you can identify the cause of the error (e.g. the requested resource does not exist), you should +consider throwing an exception with a proper HTTP status code (e.g. [[yii\web\NotFoundHttpException]] +representing a 404 HTTP status code). Yii will send the response with the corresponding HTTP status +code and text. It will also include in the response body the serialized representation of the +exception. For example, + +``` +HTTP/1.1 404 Not Found +Date: Sun, 02 Mar 2014 05:31:43 GMT +Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y +Transfer-Encoding: chunked +Content-Type: application/json; charset=UTF-8 + +{ + "type": "yii\\web\\NotFoundHttpException", + "name": "Not Found Exception", + "message": "The requested resource was not found.", + "code": 0, + "status": 404 +} +``` + +The following list summarizes the HTTP status code that are used by the Yii REST framework: + +* `200`: OK. Everything worked as expected. +* `201`: A resource was successfully created in response to a `POST` request. The `Location` header + contains the URL pointing to the newly created resource. +* `204`: The request is handled successfully and the response contains no body content (like a `DELETE` request). +* `304`: Resource was not modified. You can use the cached version. +* `400`: Bad request. This could be caused by various reasons from the user side, such as invalid JSON + data in the request body, invalid action parameters, etc. +* `401`: Authentication failed. +* `403`: The authenticated user is not allowed to access the specified API endpoint. +* `404`: The requested resource does not exist. +* `405`: Method not allowed. Please check the `Allow` header for allowed HTTP methods. +* `415`: Unsupported media type. The requested content type or version number is invalid. +* `422`: Data validation failed (in response to a `POST` request, for example). Please check the response body for detailed error messages. +* `429`: Too many requests. The request is rejected due to rate limiting. +* `500`: Internal server error. This could be caused by internal program errors. + + +Versioning +---------- + +Your APIs should be versioned. Unlike Web applications which you have full control on both client side and server side +code, for APIs you usually do not have control of the client code that consumes the APIs. Therefore, backward +compatibility (BC) of the APIs should be maintained whenever possible, and if some BC-breaking changes must be +introduced to the APIs, you should bump up the version number. You may refer to [Symantic Versioning](http://semver.org/) +for more information about designing the version numbers of your APIs. + +Regarding how to implement API versioning, a common practice is to embed the version number in the API URLs. +For example, `http://example.com/v1/users` stands for `/users` API of version 1. Another method of API versioning +which gains momentum recently is to put version numbers in the HTTP request headers, typically through the `Accept` header, +like the following: + +``` +// via a parameter +Accept: application/json; version=v1 +// via a vendor content type +Accept: application/vnd.company.myapp-v1+json +``` + +Both methods have pros and cons, and there are a lot of debates about them. Below we describe a practical strategy +of API versioning that is a kind of mix of these two methods: + +* Put each major version of API implementation in a separate module whose ID is the major version number (e.g. `v1`, `v2`). + Naturally, the API URLs will contain major version numbers. +* Within each major version (and thus within the corresponding module), use the `Accept` HTTP request header + to determine the minor version number and write conditional code to respond to the minor versions accordingly. + +For each module serving a major version, it should include the resource classes and the controller classes +serving for that specific version. To better separate code responsibility, you may keep a common set of +base resource and controller classes, and subclass them in each individual version module. Within the subclasses, +implement the concrete code such as `Model::fields()`. As a result, your code may be organized like the following: + +``` +api/ + common/ + controllers/ + UserController.php + PostController.php + models/ + User.php + Post.php + modules/ + v1/ + controllers/ + UserController.php + PostController.php + models/ + User.php + Post.php + v2/ + controllers/ + UserController.php + PostController.php + models/ + User.php + Post.php +``` + +Your application configuration would look like: + +```php +return [ + 'modules' => [ + 'v1' => [ + 'basePath' => '@app/modules/v1', + ], + 'v2' => [ + 'basePath' => '@app/modules/v2', + ], + ], + 'components' => [ + 'urlManager' => [ + 'enablePrettyUrl' => true, + 'enableStrictParsing' => true, + 'showScriptName' => false, + 'rules' => [ + ['class' => 'yii\rest\UrlRule', 'controller' => ['v1/user', 'v1/post']], + ['class' => 'yii\rest\UrlRule', 'controller' => ['v2/user', 'v2/post']], + ], + ], + ], +]; +``` + +As a result, `http://example.com/v1/users` will return the list of users in version 1, while +`http://example.com/v2/users` will return version 2 users. + +Using modules, code for different major versions can be well isolated. And it is still possible +to reuse code across modules via common base classes and other shared classes. + +To deal with minor version numbers, you may take advantage of the content type negotiation +feature provided by [[yii\rest\Controller]]: + +* Specify a list of supported minor versions (within the major version of the containing module) + via [[yii\rest\Controller::supportedVersions]]. +* Get the version number by reading [[yii\rest\Controller::version]]. +* In relevant code, such as actions, resource classes, serializers, etc., write conditional + code according to the requested minor version number. + +Since minor versions require maintaining backward compatibility, hopefully there are not much +version checks in your code. Otherwise, chances are that you may need to create a new major version. + + +Caching +------- + + +Documentation +------------- + +Testing +------- + diff --git a/docs/guide/theming.md b/docs/guide/theming.md index e627f7c..037e70c 100644 --- a/docs/guide/theming.md +++ b/docs/guide/theming.md @@ -1,7 +1,12 @@ Theming ======= -TBD +A theme is a directory of view and layout files. Each file of the theme overrides corresponding file of an application +when rendered. A single application may use multiple themes and each may provide totally different experience. At any +time only one theme can be active. + +> Note: Themes usually do not meant to be redistributed since views are too application specific. If you want to + redistribute customized look and feel consider CSS and JavaScript files in form of [asset bundles](assets.md) instead. Configuring current theme ------------------------- @@ -18,4 +23,27 @@ be in your application config file: ], ], ], -``` \ No newline at end of file +``` + +In the above `pathMap` defines where to look for view files while `baseUrl` defines base URL for resources referenced +from these files. For example, if `pathMap` is `['/web/views' => '/web/themes/basic']`, then the themed version +for a view file `/web/views/site/index.php` will be `/web/themes/basic/site/index.php`. + +Using multiple paths +-------------------- + +It is possible to map a single path to multiple paths. For example, + +```php +'pathMap' => [ + '/web/views' => [ + '/web/themes/christmas', + '/web/themes/basic', + ], +] +``` + +In this case, the view will be searched in `/web/themes/christmas/site/index.php` then if it's not found it will check +`/web/themes/basic/site/index.php`. If there's no view there as well application view will be used. + +This ability is especially useful if you want to temporary or conditionally override some views. diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index 03a5b21..612eeab 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -158,7 +158,7 @@ in controllers or widgets: ```php $content = Yii::$app->view->renderFile($viewFile, $params); // You can also explicitly create a new View instance to do the rendering -// $view = new View; +// $view = new View(); // $view->renderFile($viewFile, $params); ``` @@ -186,7 +186,7 @@ New methods called [[yii\base\Model::load()|load()] and [[yii\base\Model::loadMu introduced to simplify the data population from user inputs to a model. For example, ```php -$model = new Post; +$model = new Post(); if ($model->load($_POST)) {...} // which is equivalent to: if (isset($_POST['Post'])) { @@ -329,9 +329,9 @@ public function behaviors() 'class' => 'yii\web\AccessControl', 'rules' => [ ['allow' => true, 'actions' => ['admin'], 'roles' => ['@']], - ), - ), - ); + ], + ], + ]; } ``` @@ -394,7 +394,7 @@ In 1.1, query building is scattered among several classes, including `CDbCommand and [[yii\db\QueryBuilder|QueryBuilder]] to generate SQL statements from query objects. For example: ```php -$query = new \yii\db\Query; +$query = new \yii\db\Query(); $query->select('id, name') ->from('tbl_user') ->limit(10); diff --git a/extensions/apidoc/composer.json b/extensions/apidoc/composer.json index 69f9c4b..6c0c5ce 100644 --- a/extensions/apidoc/composer.json +++ b/extensions/apidoc/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-apidoc", "description": "API Documentation generator for the Yii framework 2.0", - "keywords": ["yii", "phpdoc", "apidoc", "api", "documentation"], + "keywords": ["yii2", "phpdoc", "apidoc", "api", "documentation"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { @@ -22,6 +22,7 @@ "yiisoft/yii2": "*", "yiisoft/yii2-bootstrap": "*", "phpdocumentor/reflection": ">=1.0.3", + "phpdocumentor/reflection-docblock": ">2.0.1", "nikic/php-parser": "0.9.*" }, "autoload": { diff --git a/extensions/apidoc/models/PropertyDoc.php b/extensions/apidoc/models/PropertyDoc.php index 85c482c..fc7d99d 100644 --- a/extensions/apidoc/models/PropertyDoc.php +++ b/extensions/apidoc/models/PropertyDoc.php @@ -69,7 +69,7 @@ class PropertyDoc extends BaseDoc $this->types = $tag->getTypes(); $this->description = ucfirst($tag->getDescription()); if (($pos = strpos($this->description, '.')) !== false) { - $this->shortDescription = substr($this->description, 0, $pos); + $this->shortDescription = substr($this->description, 0, $pos + 1); } else { $this->shortDescription = $this->description; } diff --git a/extensions/apidoc/templates/bootstrap/assets/css/style.css b/extensions/apidoc/templates/bootstrap/assets/css/style.css index 57e3de9..6240146 100644 --- a/extensions/apidoc/templates/bootstrap/assets/css/style.css +++ b/extensions/apidoc/templates/bootstrap/assets/css/style.css @@ -50,3 +50,11 @@ body { background: #E6ECFF; border: 1px #BFCFFF solid; } + +blockquote { + font-size: 14px; +} + +td p { + margin: 0; +} \ No newline at end of file diff --git a/extensions/apidoc/templates/html/views/constSummary.php b/extensions/apidoc/templates/html/views/constSummary.php index 6e5aabc..e2b8b02 100644 --- a/extensions/apidoc/templates/html/views/constSummary.php +++ b/extensions/apidoc/templates/html/views/constSummary.php @@ -36,7 +36,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><?= $renderer->createTypeLink($constant->definedBy) ?></td> </tr> <?php endforeach; ?> diff --git a/extensions/apidoc/templates/html/views/eventDetails.php b/extensions/apidoc/templates/html/views/eventDetails.php index dd4293e..46fc5bd 100644 --- a/extensions/apidoc/templates/html/views/eventDetails.php +++ b/extensions/apidoc/templates/html/views/eventDetails.php @@ -18,7 +18,7 @@ ArrayHelper::multisort($events, 'name'); <h2>Event Details</h2> <?php foreach($events as $event): ?> <div class="detailHeader h3" id="<?= $event->name.'-detail' ?>"> - <?php echo $event->name; ?> + <?= $event->name ?> <span class="detailHeaderTag small"> event <?php if(!empty($event->since)): ?> @@ -32,7 +32,7 @@ ArrayHelper::multisort($events, 'name'); <?php echo $event->trigger->signature; ?> </div>*/ ?> - <p><?= ApiMarkdown::process($event->description, $type); ?></p> + <?= ApiMarkdown::process($event->description, $type); ?> <?= $this->render('seeAlso', ['object' => $event]); ?> diff --git a/extensions/apidoc/templates/html/views/eventSummary.php b/extensions/apidoc/templates/html/views/eventSummary.php index 39e9ab5..c010be4 100644 --- a/extensions/apidoc/templates/html/views/eventSummary.php +++ b/extensions/apidoc/templates/html/views/eventSummary.php @@ -39,11 +39,11 @@ ArrayHelper::multisort($events, 'name'); <td> <?= ApiMarkdown::process($event->shortDescription, $event->definedBy, true) ?> <?php if(!empty($event->since)): ?> - (available since version <?php echo $event->since; ?>) + (available since version <?= $event->since ?>) <?php endif; ?> </td> <td><?= $renderer->createTypeLink($event->definedBy) ?></td> </tr> <?php endforeach; ?> </table> -</div> \ No newline at end of file +</div> diff --git a/extensions/apidoc/templates/html/views/methodDetails.php b/extensions/apidoc/templates/html/views/methodDetails.php index 61c2d4f..16b043e 100644 --- a/extensions/apidoc/templates/html/views/methodDetails.php +++ b/extensions/apidoc/templates/html/views/methodDetails.php @@ -65,8 +65,8 @@ ArrayHelper::multisort($methods, 'name'); <!-- --><?php //$this->renderPartial('sourceCode',array('object'=>$method)); ?> - <p><?= ApiMarkdown::process($method->shortDescription, $type, true) ?></strong></p> - <p><?= ApiMarkdown::process($method->description, $type) ?></p> + <p><strong><?= ApiMarkdown::process($method->shortDescription, $type, true) ?></strong></p> + <?= ApiMarkdown::process($method->description, $type) ?> <?= $this->render('seeAlso', ['object' => $method]); ?> diff --git a/extensions/apidoc/templates/html/views/propertyDetails.php b/extensions/apidoc/templates/html/views/propertyDetails.php index 685fcb9..6df09fe 100644 --- a/extensions/apidoc/templates/html/views/propertyDetails.php +++ b/extensions/apidoc/templates/html/views/propertyDetails.php @@ -24,7 +24,7 @@ ArrayHelper::multisort($properties, 'name'); <?php foreach($properties as $property): ?> <div class="detailHeader h3" id="<?= $property->name.'-detail' ?>"> - <?php echo $property->name; ?> + <?= $property->name ?> <span class="detailHeaderTag small"> <?= $property->visibility ?> <?php if($property->getIsReadOnly()) echo ' <em>read-only</em> '; ?> @@ -38,7 +38,7 @@ ArrayHelper::multisort($properties, 'name'); <div class="signature"><?php echo $renderer->renderPropertySignature($property); ?></div> - <p><?= ApiMarkdown::process($property->description, $type) ?></p> + <?= ApiMarkdown::process($property->description, $type) ?> <?= $this->render('seeAlso', ['object' => $property]); ?> diff --git a/extensions/authclient/composer.json b/extensions/authclient/composer.json index 60290cb..81c5aa1 100644 --- a/extensions/authclient/composer.json +++ b/extensions/authclient/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-authclient", "description": "External authentication via OAuth and OpenID for the Yii framework", - "keywords": ["yii", "OAuth", "OpenID", "auth"], + "keywords": ["yii2", "OAuth", "OpenID", "auth"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/bootstrap/CHANGELOG.md b/extensions/bootstrap/CHANGELOG.md index 5fced15..464c959 100644 --- a/extensions/bootstrap/CHANGELOG.md +++ b/extensions/bootstrap/CHANGELOG.md @@ -12,9 +12,11 @@ Yii Framework 2 bootstrap extension Change Log - Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) - Enh #1881: Improved `yii\bootstrap\NavBar` with `containerOptions`, `innerContainerOptions` and `renderInnerContainer` (creocoder) - Enh #2425: Tabs widget now selects first tab if no active tab is specified (samdark) +- Enh #2643: Add size attribute to Modal (tof06) - Chg #1459: Update Collapse to use bootstrap 3 classes (tonydspaniard) - Chg #1820: Update Progress to use bootstrap 3 markup (samdark) + 2.0.0 alpha, December 1, 2013 ----------------------------- diff --git a/extensions/bootstrap/Modal.php b/extensions/bootstrap/Modal.php index 276b57c..e498f49 100644 --- a/extensions/bootstrap/Modal.php +++ b/extensions/bootstrap/Modal.php @@ -35,6 +35,10 @@ use yii\helpers\Html; */ class Modal extends Widget { + const SIZE_LARGE = "modal-lg"; + const SIZE_SMALL = "modal-sm"; + const SIZE_DEFAULT = ""; + /** * @var string the header content in the modal window. */ @@ -44,6 +48,10 @@ class Modal extends Widget */ public $footer; /** + * @var string the modal size. Can be MODAL_LG or MODAL_SM, or empty for default. + */ + public $size; + /** * @var array the options for rendering the close button tag. * The close button is displayed in the header of the modal window. Clicking * on the button will hide the modal window. If this is null, no close button will be rendered. @@ -86,7 +94,7 @@ class Modal extends Widget echo $this->renderToggleButton() . "\n"; echo Html::beginTag('div', $this->options) . "\n"; - echo Html::beginTag('div', ['class' => 'modal-dialog']) . "\n"; + echo Html::beginTag('div', ['class' => 'modal-dialog ' . $this->size]) . "\n"; echo Html::beginTag('div', ['class' => 'modal-content']) . "\n"; echo $this->renderHeader() . "\n"; echo $this->renderBodyBegin() . "\n"; diff --git a/extensions/bootstrap/Tabs.php b/extensions/bootstrap/Tabs.php index 2192746..29505f0 100644 --- a/extensions/bootstrap/Tabs.php +++ b/extensions/bootstrap/Tabs.php @@ -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'; diff --git a/extensions/bootstrap/composer.json b/extensions/bootstrap/composer.json index e2a99df..637a084 100644 --- a/extensions/bootstrap/composer.json +++ b/extensions/bootstrap/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-bootstrap", "description": "The Twitter Bootstrap extension for the Yii framework", - "keywords": ["yii", "bootstrap"], + "keywords": ["yii2", "bootstrap"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/codeception/composer.json b/extensions/codeception/composer.json index 0982d8d..fa99992 100644 --- a/extensions/codeception/composer.json +++ b/extensions/codeception/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-codeception", "description": "The Codeception integration for the Yii framework", - "keywords": ["yii", "codeception"], + "keywords": ["yii2", "codeception"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/composer/composer.json b/extensions/composer/composer.json index 38f5199..f311581 100644 --- a/extensions/composer/composer.json +++ b/extensions/composer/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-composer", "description": "The composer plugin for Yii extension installer", - "keywords": ["yii", "composer", "extension installer"], + "keywords": ["yii2", "composer", "extension installer"], "type": "composer-plugin", "license": "BSD-3-Clause", "support": { diff --git a/extensions/debug/composer.json b/extensions/debug/composer.json index 84dafd5..3d095b1 100644 --- a/extensions/debug/composer.json +++ b/extensions/debug/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-debug", "description": "The debugger extension for the Yii framework", - "keywords": ["yii", "debug", "debugger"], + "keywords": ["yii2", "debug", "debugger"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/debug/models/search/Mail.php b/extensions/debug/models/search/Mail.php index 464436a..3cd7a27 100644 --- a/extensions/debug/models/search/Mail.php +++ b/extensions/debug/models/search/Mail.php @@ -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'], diff --git a/extensions/debug/views/default/panels/config/detail.php b/extensions/debug/views/default/panels/config/detail.php index 88e96a1..b2cfde1 100644 --- a/extensions/debug/views/default/panels/config/detail.php +++ b/extensions/debug/views/default/panels/config/detail.php @@ -1,6 +1,4 @@ <?php -use yii\helpers\Html; - /** * @var yii\debug\panels\ConfigPanel $panel */ diff --git a/extensions/debug/views/default/panels/config/summary.php b/extensions/debug/views/default/panels/config/summary.php index af72260..bb3a6dd 100644 --- a/extensions/debug/views/default/panels/config/summary.php +++ b/extensions/debug/views/default/panels/config/summary.php @@ -1,7 +1,4 @@ <?php - -use yii\helpers\Html; - /** * @var yii\debug\panels\ConfigPanel $panel */ diff --git a/extensions/debug/views/default/panels/db/summary.php b/extensions/debug/views/default/panels/db/summary.php index dcf49d8..022f9a7 100644 --- a/extensions/debug/views/default/panels/db/summary.php +++ b/extensions/debug/views/default/panels/db/summary.php @@ -1,6 +1,6 @@ <?php if ($queryCount): ?> <div class="yii-debug-toolbar-block"> - <a href="<?= $panel->getUrl() ?>" title="Executed <?php echo $queryCount; ?> database queries which took <?= $queryTime ?>."> + <a href="<?= $panel->getUrl() ?>" title="Executed <?= $queryCount ?> database queries which took <?= $queryTime ?>."> DB <span class="label label-info"><?= $queryCount ?></span> <span class="label"><?= $queryTime ?></span> </a> </div> diff --git a/extensions/elasticsearch/README.md b/extensions/elasticsearch/README.md index 63df0eb..ba6c168 100644 --- a/extensions/elasticsearch/README.md +++ b/extensions/elasticsearch/README.md @@ -50,9 +50,6 @@ TBD > **NOTE:** elasticsearch limits the number of records returned by any query to 10 records by default. > If you expect to get more records you should specify limit explicitly in relation definition. - * This is also important for relations that use [[via()]] so that if via records are limited to 10 - * the relations records can also not be more than 10. - * Using the ActiveRecord @@ -60,14 +57,15 @@ Using the ActiveRecord For general information on how to use yii's ActiveRecord please refer to the [guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). -For defining an elasticsearch ActiveRecord class your record class needs to extend from `yii\elasticsearch\ActiveRecord` and -implement at least the `attributes()` method to define the attributes of the record. +For defining an elasticsearch ActiveRecord class your record class needs to extend from [[yii\elasticsearch\ActiveRecord]] and +implement at least the [[yii\elasticsearch\ActiveRecord::attributes()|attributes()]] method to define the attributes of the record. The handling of primary keys is different in elasticsearch as the primary key (the `_id` field in elasticsearch terms) is not part of the attributes by default. However it is possible to define a [path mapping](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html) for the `_id` field to be part of the attributes. See [elasticsearch docs](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html) on how to define it. -The `_id` field of a document/record can be accessed using [[ActiveRecord::getPrimaryKey()]] and [[ActiveRecord::setPrimaryKey()]]. -When path mapping is defined, the attribute name can be defined using the [[primaryKey()]] method. +The `_id` field of a document/record can be accessed using [[yii\elasticsearch\ActiveRecord::getPrimaryKey()|getPrimaryKey()]] and +[[yii\elasticsearch\ActiveRecord::setPrimaryKey()|setPrimaryKey()]]. +When path mapping is defined, the attribute name can be defined using the [[yii\elasticsearch\ActiveRecord::primaryKey()|primaryKey()]] method. The following is an example model called `Customer`: @@ -101,7 +99,8 @@ class Customer extends \yii\elasticsearch\ActiveRecord } ``` -You may override [[index()]] and [[type()]] to define the index and type this record represents. +You may override [[yii\elasticsearch\ActiveRecord::index()|index()]] and [[yii\elasticsearch\ActiveRecord::type()|type()]] +to define the index and type this record represents. The general usage of elasticsearch ActiveRecord is very similar to the database ActiveRecord as described in the [guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). @@ -109,13 +108,18 @@ It supports the same interface and features except the following limitations and - As elasticsearch does not support SQL, the query API does not support `join()`, `groupBy()`, `having()` and `union()`. Sorting, limit, offset and conditional where are all supported. -- `from()` does not select the tables, but the [index](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-index) +- [[yii\elasticsearch\ActiveQuery::from()|from()]] does not select the tables, but the + [index](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-index) and [type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-type) to query against. -- `select()` has been replaced with `fields()` which basically does the same but `fields` is more elasticsearch terminology. +- `select()` has been replaced with [[yii\elasticsearch\ActiveQuery::fields()|fields()]] which basically does the same but + `fields` is more elasticsearch terminology. It defines the fields to retrieve from a document. -- `via`-relations can not be defined via a table as there are no tables in elasticsearch. You can only define relations via other records. -- As elasticsearch is not only a data storage but also a search engine there is of course support added for search your records. - There are `query()`, `filter()` and `addFacets()` methods that allows to compose an elasticsearch query. +- [[yii\elasticsearch\ActiveQuery::via()|via]]-relations can not be defined via a table as there are no tables in elasticsearch. You can only define relations via other records. +- As elasticsearch is not only a data storage but also a search engine there is of course support added for searching your records. + There are + [[yii\elasticsearch\ActiveQuery::query()|query()]], + [[yii\elasticsearch\ActiveQuery::filter()|filter()]] and + [[yii\elasticsearch\ActiveQuery::addFacet()|addFacet()]] methods that allows to compose an elasticsearch query. See the usage example below on how they work and check out the [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) on how to compose `query` and `filter` parts. - It is also possible to define relations from elasticsearch ActiveRecords to normal ActiveRecord classes and vice versa. diff --git a/extensions/elasticsearch/composer.json b/extensions/elasticsearch/composer.json index 097b102..d38454f 100644 --- a/extensions/elasticsearch/composer.json +++ b/extensions/elasticsearch/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-elasticsearch", "description": "Elasticsearch integration and ActiveRecord for the Yii framework", - "keywords": ["yii", "elasticsearch", "active-record", "search", "fulltext"], + "keywords": ["yii2", "elasticsearch", "active-record", "search", "fulltext"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/faker/composer.json b/extensions/faker/composer.json index 5af5319..a8a5bc1 100644 --- a/extensions/faker/composer.json +++ b/extensions/faker/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-faker", "description": "Fixture generator. The Faker integration for the Yii framework.", - "keywords": ["yii", "faker", "fixture"], + "keywords": ["yii2", "faker", "fixture"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/gii/CHANGELOG.md b/extensions/gii/CHANGELOG.md index 8a7632d..c67f8f4 100644 --- a/extensions/gii/CHANGELOG.md +++ b/extensions/gii/CHANGELOG.md @@ -13,6 +13,8 @@ Yii Framework 2 gii extension Change Log - Enh #1897: diff markup is now copy paste friendly (samdark) - Enh #2327: better visual representation of changed files, added header and refresh button to diff modal (thiagotalma) - Enh #2491: Added support for using the same base class name of search model and data model in Gii (qiangxue) +- Enh #2595: Browse through all generated files using right and left arrows (thiagotalma) +- Enh #2633: Keyboard shortcuts to browse through files (thiagotalma) 2.0.0 alpha, December 1, 2013 ----------------------------- diff --git a/extensions/gii/Module.php b/extensions/gii/Module.php index 30302b5..0b0f6e4 100644 --- a/extensions/gii/Module.php +++ b/extensions/gii/Module.php @@ -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'], ]; } } diff --git a/extensions/gii/assets/gii.js b/extensions/gii/assets/gii.js index c5a70ab..282ce58 100644 --- a/extensions/gii/assets/gii.js +++ b/extensions/gii/assets/gii.js @@ -35,10 +35,13 @@ yii.gii = (function ($) { }; var initPreviewDiffLinks = function () { - $('.preview-code, .diff-code, .modal-refresh').on('click', function () { + $('.preview-code, .diff-code, .modal-refresh, .modal-previous, .modal-next').on('click', function () { var $modal = $('#preview-modal'); var $link = $(this); - $modal.find('.modal-refresh').attr('href', $link.prop('href')); + $modal.find('.modal-refresh').attr('href', $link.attr('href')); + if ($link.hasClass('preview-code') || $link.hasClass('diff-code')) { + $modal.data('action', ($link.hasClass('preview-code') ? 'preview-code' : 'diff-code')) + } $modal.find('.modal-title').text($link.data('title')); $modal.find('.modal-body').html('Loading ...'); $modal.modal('show'); @@ -48,6 +51,15 @@ yii.gii = (function ($) { url: $link.prop('href'), data: $('.default-view form').serializeArray(), success: function (data) { + if (!$link.hasClass('modal-refresh')) { + var filesSelector = 'a.' + $modal.data('action'); + var $files = $(filesSelector); + var index = $files.filter('[href="' + $link.attr('href') + '"]').index(filesSelector); + var $prev = $files.eq(index-1); + var $next = $files.eq((index+1 == $files.length ? 0 : index+1)); + $modal.find('.modal-previous').attr('href', $prev.attr('href')).data('title', $prev.data('title')); + $modal.find('.modal-next').attr('href', $next.attr('href')).data('title', $next.data('title')); + } $modal.find('.modal-body').html(data); $modal.find('.content').css('max-height', ($(window).height() - 200) + 'px'); }, @@ -57,6 +69,16 @@ yii.gii = (function ($) { }); return false; }); + + $('#preview-modal').on('keydown', function(e) { + if (e.keyCode === 37) { + $('.modal-previous').trigger('click'); + } else if(e.keyCode === 39) { + $('.modal-next').trigger('click'); + } else if(e.keyCode === 82) { + $('.modal-refresh').trigger('click'); + } + }); }; var initConfirmationCheckboxes = function () { diff --git a/extensions/gii/composer.json b/extensions/gii/composer.json index dad7f87..11fa779 100644 --- a/extensions/gii/composer.json +++ b/extensions/gii/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-gii", "description": "The Gii extension for the Yii framework", - "keywords": ["yii", "gii", "code generator"], + "keywords": ["yii2", "gii", "code generator"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/gii/generators/controller/templates/view.php b/extensions/gii/generators/controller/templates/view.php index 6ef2a87..a458b16 100644 --- a/extensions/gii/generators/controller/templates/view.php +++ b/extensions/gii/generators/controller/templates/view.php @@ -18,5 +18,5 @@ echo "<?php\n"; <p> You may change the content of this page by modifying - the file <code><?= '<?php' ?> echo __FILE__; ?></code>. + the file <code><?= '<?=' ?> __FILE__; ?></code>. </p> diff --git a/extensions/gii/generators/crud/Generator.php b/extensions/gii/generators/crud/Generator.php index 8a8cb1c..dd0cb08 100644 --- a/extensions/gii/generators/crud/Generator.php +++ b/extensions/gii/generators/crud/Generator.php @@ -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 diff --git a/extensions/gii/generators/crud/templates/views/create.php b/extensions/gii/generators/crud/templates/views/create.php index 68d08ba..72a1f8d 100644 --- a/extensions/gii/generators/crud/templates/views/create.php +++ b/extensions/gii/generators/crud/templates/views/create.php @@ -26,8 +26,8 @@ $this->params['breadcrumbs'][] = $this->title; <h1><?= "<?= " ?>Html::encode($this->title) ?></h1> - <?= "<?php " ?>echo $this->render('_form', [ + <?= "<?= " ?>$this->render('_form', [ 'model' => $model, - ]); ?> + ]) ?> </div> diff --git a/extensions/gii/generators/crud/templates/views/index.php b/extensions/gii/generators/crud/templates/views/index.php index a4f5f0d..aadc586 100644 --- a/extensions/gii/generators/crud/templates/views/index.php +++ b/extensions/gii/generators/crud/templates/views/index.php @@ -37,7 +37,7 @@ $this->params['breadcrumbs'][] = $this->title; </p> <?php if ($generator->indexWidgetType === 'grid'): ?> - <?= "<?php " ?>echo GridView::widget([ + <?= "<?= " ?>GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ @@ -69,13 +69,13 @@ if (($tableSchema = $generator->getTableSchema()) === false) { ], ]); ?> <?php else: ?> - <?= "<?php " ?>echo ListView::widget([ + <?= "<?= " ?>ListView::widget([ 'dataProvider' => $dataProvider, 'itemOptions' => ['class' => 'item'], 'itemView' => function ($model, $key, $index, $widget) { return Html::a(Html::encode($model-><?= $nameAttribute ?>), ['view', <?= $urlParams ?>]); }, - ]); ?> + ]) ?> <?php endif; ?> </div> diff --git a/extensions/gii/generators/crud/templates/views/update.php b/extensions/gii/generators/crud/templates/views/update.php index 2fbbecf..610b5bb 100644 --- a/extensions/gii/generators/crud/templates/views/update.php +++ b/extensions/gii/generators/crud/templates/views/update.php @@ -29,8 +29,8 @@ $this->params['breadcrumbs'][] = 'Update'; <h1><?= "<?= " ?>Html::encode($this->title) ?></h1> - <?= "<?php " ?>echo $this->render('_form', [ + <?= "<?= " ?>$this->render('_form', [ 'model' => $model, - ]); ?> + ]) ?> </div> diff --git a/extensions/gii/generators/crud/templates/views/view.php b/extensions/gii/generators/crud/templates/views/view.php index 989dab5..8ffa728 100644 --- a/extensions/gii/generators/crud/templates/views/view.php +++ b/extensions/gii/generators/crud/templates/views/view.php @@ -31,16 +31,16 @@ $this->params['breadcrumbs'][] = $this->title; <p> <?= "<?= " ?>Html::a('Update', ['update', <?= $urlParams ?>], ['class' => 'btn btn-primary']) ?> - <?= "<?php " ?>echo Html::a('Delete', ['delete', <?= $urlParams ?>], [ + <?= "<?= " ?>Html::a('Delete', ['delete', <?= $urlParams ?>], [ 'class' => 'btn btn-danger', 'data' => [ 'confirm' => Yii::t('app', 'Are you sure to delete this item?'), 'method' => 'post', ], - ]); ?> + ]) ?> </p> - <?= "<?php " ?>echo DetailView::widget([ + <?= "<?= " ?>DetailView::widget([ 'model' => $model, 'attributes' => [ <?php @@ -56,6 +56,6 @@ if (($tableSchema = $generator->getTableSchema()) === false) { } ?> ], - ]); ?> + ]) ?> </div> diff --git a/extensions/gii/generators/extension/Generator.php b/extensions/gii/generators/extension/Generator.php new file mode 100644 index 0000000..094d476 --- /dev/null +++ b/extensions/gii/generators/extension/Generator.php @@ -0,0 +1,264 @@ +<?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); + } +} diff --git a/extensions/gii/generators/extension/form.php b/extensions/gii/generators/extension/form.php new file mode 100644 index 0000000..98a250c --- /dev/null +++ b/extensions/gii/generators/extension/form.php @@ -0,0 +1,27 @@ +<?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> diff --git a/extensions/gii/generators/extension/templates/AutoloadExample.php b/extensions/gii/generators/extension/templates/AutoloadExample.php new file mode 100644 index 0000000..194ba72 --- /dev/null +++ b/extensions/gii/generators/extension/templates/AutoloadExample.php @@ -0,0 +1,14 @@ +<?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!"; + } +} diff --git a/extensions/gii/generators/extension/templates/README.md b/extensions/gii/generators/extension/templates/README.md new file mode 100644 index 0000000..08101f3 --- /dev/null +++ b/extensions/gii/generators/extension/templates/README.md @@ -0,0 +1,35 @@ +<?= $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 diff --git a/extensions/gii/generators/extension/templates/composer.json b/extensions/gii/generators/extension/templates/composer.json new file mode 100644 index 0000000..941a9a5 --- /dev/null +++ b/extensions/gii/generators/extension/templates/composer.json @@ -0,0 +1,18 @@ +{ + "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) ?>": "" + } + } +} diff --git a/extensions/gii/generators/model/Generator.php b/extensions/gii/generators/model/Generator.php index 538637c..893cd7f 100644 --- a/extensions/gii/generators/model/Generator.php +++ b/extensions/gii/generators/model/Generator.php @@ -471,7 +471,7 @@ class Generator extends \yii\gii\Generator */ public function validateTableName() { - if (($pos = strpos($this->tableName, '*')) !== false && substr($this->tableName, -1) !== '*') { + if (strpos($this->tableName, '*') !== false && substr($this->tableName, -1) !== '*') { $this->addError('tableName', 'Asterisk is only allowed as the last character.'); return; } diff --git a/extensions/gii/views/default/view/files.php b/extensions/gii/views/default/view/files.php index 947c8f8..fbe5e69 100644 --- a/extensions/gii/views/default/view/files.php +++ b/extensions/gii/views/default/view/files.php @@ -81,7 +81,14 @@ use yii\gii\CodeFile; <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h4><a class="modal-refresh glyphicon glyphicon-refresh" href="#"></a> <span class="modal-title">Modal title</span></h4> + <div class="btn-group pull-left"> + <a class="modal-previous btn btn-xs btn-default" href="#" title="Previous File (Left Arrow)"><span class="glyphicon glyphicon-arrow-left"></span></a> + <a class="modal-next btn btn-xs btn-default" href="#" title="Next File (Right Arrow)"><span class="glyphicon glyphicon-arrow-right"></span></a> + <a class="modal-refresh btn btn-xs btn-default" href="#" title="Refresh File (R)"><span class="glyphicon glyphicon-refresh"></span></a> + + </div> + <strong class="modal-title pull-left">Modal title</strong> + <div class="clearfix"></div> </div> <div class="modal-body"> <p>Please wait ...</p> diff --git a/extensions/imagine/composer.json b/extensions/imagine/composer.json index e032ff9..84f227c 100644 --- a/extensions/imagine/composer.json +++ b/extensions/imagine/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-imagine", "description": "The Imagine integration for the Yii framework", - "keywords": ["yii", "imagine", "image", "helper"], + "keywords": ["yii2", "imagine", "image", "helper"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/jui/CHANGELOG.md b/extensions/jui/CHANGELOG.md index 55c5e33..2730ae3 100644 --- a/extensions/jui/CHANGELOG.md +++ b/extensions/jui/CHANGELOG.md @@ -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 ----------------------------- diff --git a/extensions/jui/DatePicker.php b/extensions/jui/DatePicker.php index 6fc73bb..fff6582 100644 --- a/extensions/jui/DatePicker.php +++ b/extensions/jui/DatePicker.php @@ -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 { diff --git a/extensions/jui/assets/jquery.ui.datepicker-i18n.js b/extensions/jui/assets/jquery.ui.datepicker-i18n.js old mode 100755 new mode 100644 diff --git a/extensions/jui/assets/jquery.ui.effect-all.js b/extensions/jui/assets/jquery.ui.effect-all.js old mode 100755 new mode 100644 diff --git a/extensions/jui/assets/theme/images/animated-overlay.gif b/extensions/jui/assets/theme/images/animated-overlay.gif old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/animated-overlay.gif and b/extensions/jui/assets/theme/images/animated-overlay.gif differ diff --git a/extensions/jui/assets/theme/images/ui-bg_flat_0_aaaaaa_40x100.png b/extensions/jui/assets/theme/images/ui-bg_flat_0_aaaaaa_40x100.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-bg_flat_0_aaaaaa_40x100.png and b/extensions/jui/assets/theme/images/ui-bg_flat_0_aaaaaa_40x100.png differ diff --git a/extensions/jui/assets/theme/images/ui-bg_flat_75_ffffff_40x100.png b/extensions/jui/assets/theme/images/ui-bg_flat_75_ffffff_40x100.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-bg_flat_75_ffffff_40x100.png and b/extensions/jui/assets/theme/images/ui-bg_flat_75_ffffff_40x100.png differ diff --git a/extensions/jui/assets/theme/images/ui-bg_glass_55_fbf9ee_1x400.png b/extensions/jui/assets/theme/images/ui-bg_glass_55_fbf9ee_1x400.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-bg_glass_55_fbf9ee_1x400.png and b/extensions/jui/assets/theme/images/ui-bg_glass_55_fbf9ee_1x400.png differ diff --git a/extensions/jui/assets/theme/images/ui-bg_glass_65_ffffff_1x400.png b/extensions/jui/assets/theme/images/ui-bg_glass_65_ffffff_1x400.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-bg_glass_65_ffffff_1x400.png and b/extensions/jui/assets/theme/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/extensions/jui/assets/theme/images/ui-bg_glass_75_dadada_1x400.png b/extensions/jui/assets/theme/images/ui-bg_glass_75_dadada_1x400.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-bg_glass_75_dadada_1x400.png and b/extensions/jui/assets/theme/images/ui-bg_glass_75_dadada_1x400.png differ diff --git a/extensions/jui/assets/theme/images/ui-bg_glass_75_e6e6e6_1x400.png b/extensions/jui/assets/theme/images/ui-bg_glass_75_e6e6e6_1x400.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-bg_glass_75_e6e6e6_1x400.png and b/extensions/jui/assets/theme/images/ui-bg_glass_75_e6e6e6_1x400.png differ diff --git a/extensions/jui/assets/theme/images/ui-bg_glass_95_fef1ec_1x400.png b/extensions/jui/assets/theme/images/ui-bg_glass_95_fef1ec_1x400.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-bg_glass_95_fef1ec_1x400.png and b/extensions/jui/assets/theme/images/ui-bg_glass_95_fef1ec_1x400.png differ diff --git a/extensions/jui/assets/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/extensions/jui/assets/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png and b/extensions/jui/assets/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ diff --git a/extensions/jui/assets/theme/images/ui-icons_222222_256x240.png b/extensions/jui/assets/theme/images/ui-icons_222222_256x240.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-icons_222222_256x240.png and b/extensions/jui/assets/theme/images/ui-icons_222222_256x240.png differ diff --git a/extensions/jui/assets/theme/images/ui-icons_2e83ff_256x240.png b/extensions/jui/assets/theme/images/ui-icons_2e83ff_256x240.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-icons_2e83ff_256x240.png and b/extensions/jui/assets/theme/images/ui-icons_2e83ff_256x240.png differ diff --git a/extensions/jui/assets/theme/images/ui-icons_454545_256x240.png b/extensions/jui/assets/theme/images/ui-icons_454545_256x240.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-icons_454545_256x240.png and b/extensions/jui/assets/theme/images/ui-icons_454545_256x240.png differ diff --git a/extensions/jui/assets/theme/images/ui-icons_888888_256x240.png b/extensions/jui/assets/theme/images/ui-icons_888888_256x240.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-icons_888888_256x240.png and b/extensions/jui/assets/theme/images/ui-icons_888888_256x240.png differ diff --git a/extensions/jui/assets/theme/images/ui-icons_cd0a0a_256x240.png b/extensions/jui/assets/theme/images/ui-icons_cd0a0a_256x240.png old mode 100755 new mode 100644 Binary files a/extensions/jui/assets/theme/images/ui-icons_cd0a0a_256x240.png and b/extensions/jui/assets/theme/images/ui-icons_cd0a0a_256x240.png differ diff --git a/extensions/jui/assets/theme/jquery.ui.css b/extensions/jui/assets/theme/jquery.ui.css old mode 100755 new mode 100644 diff --git a/extensions/jui/composer.json b/extensions/jui/composer.json index 126df39..f129fef 100644 --- a/extensions/jui/composer.json +++ b/extensions/jui/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-jui", "description": "The Jquery UI extension for the Yii framework", - "keywords": ["yii", "Jquery UI", "renderer"], + "keywords": ["yii2", "Jquery UI", "renderer"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/mongodb/Collection.php b/extensions/mongodb/Collection.php index 38144bc..6f595f8 100644 --- a/extensions/mongodb/Collection.php +++ b/extensions/mongodb/Collection.php @@ -260,7 +260,7 @@ class Collection extends Object } /** - * Returns a a single document. + * Returns a single document. * @param array $condition query condition * @param array $fields fields to be selected * @return array|null the single document. Null is returned if the query results in nothing. @@ -272,6 +272,32 @@ class Collection extends Object } /** + * Updates a document and returns it. + * @param array $condition query condition + * @param array $update update criteria + * @param array $fields fields to be returned + * @param array $options list of options in format: optionName => optionValue. + * @return array|null the original document, or the modified document when $options['new'] is set. + * @throws Exception on failure. + * @see http://www.php.net/manual/en/mongocollection.findandmodify.php + */ + public function findAndModify($condition, $update, $fields = [], $options = []) + { + $condition = $this->buildCondition($condition); + $token = $this->composeLogToken('findAndModify', [$condition, $update, $fields, $options]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->findAndModify($condition, $update, $fields, $options); + Yii::endProfile($token, __METHOD__); + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int)$e->getCode(), $e); + } + } + + /** * Inserts new data into collection. * @param array|object $data data to be inserted. * @param array $options list of options in format: optionName => optionValue. diff --git a/extensions/mongodb/composer.json b/extensions/mongodb/composer.json index d69a1a1..6556bd0 100644 --- a/extensions/mongodb/composer.json +++ b/extensions/mongodb/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-mongodb", "description": "MongoDb extension for the Yii framework", - "keywords": ["yii", "mongo", "mongodb", "active-record"], + "keywords": ["yii2", "mongo", "mongodb", "active-record"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/redis/LuaScriptBuilder.php b/extensions/redis/LuaScriptBuilder.php index 3da5d3d..1c1082b 100644 --- a/extensions/redis/LuaScriptBuilder.php +++ b/extensions/redis/LuaScriptBuilder.php @@ -7,6 +7,7 @@ namespace yii\redis; +use yii\base\InvalidParamException; use yii\base\NotSupportedException; use yii\db\Exception; use yii\db\Expression; diff --git a/extensions/redis/composer.json b/extensions/redis/composer.json index 3740013..7561028 100644 --- a/extensions/redis/composer.json +++ b/extensions/redis/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-redis", "description": "Redis Cache, Session and ActiveRecord for the Yii framework", - "keywords": ["yii", "redis", "active-record", "cache", "session"], + "keywords": ["yii2", "redis", "active-record", "cache", "session"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/smarty/composer.json b/extensions/smarty/composer.json index 16b48bf..7d32b6d 100644 --- a/extensions/smarty/composer.json +++ b/extensions/smarty/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-smarty", "description": "The Smarty integration for the Yii framework", - "keywords": ["yii", "smarty", "renderer"], + "keywords": ["yii2", "smarty", "renderer"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/sphinx/QueryBuilder.php b/extensions/sphinx/QueryBuilder.php index e24a3a8..8e1d5c4 100644 --- a/extensions/sphinx/QueryBuilder.php +++ b/extensions/sphinx/QueryBuilder.php @@ -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), diff --git a/extensions/sphinx/composer.json b/extensions/sphinx/composer.json index 9f31226..db3e928 100644 --- a/extensions/sphinx/composer.json +++ b/extensions/sphinx/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-sphinx", "description": "Sphinx full text search engine extension for the Yii framework", - "keywords": ["yii", "sphinx", "active-record", "search", "fulltext"], + "keywords": ["yii2", "sphinx", "active-record", "search", "fulltext"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/swiftmailer/Mailer.php b/extensions/swiftmailer/Mailer.php index 891f2b5..8a87ba2 100644 --- a/extensions/swiftmailer/Mailer.php +++ b/extensions/swiftmailer/Mailer.php @@ -19,7 +19,7 @@ use yii\mail\BaseMailer; * ~~~ * 'components' => [ * ... - * 'email' => [ + * 'mail' => [ * 'class' => 'yii\swiftmailer\Mailer', * 'transport' => [ * 'class' => 'Swift_SmtpTransport', diff --git a/extensions/swiftmailer/composer.json b/extensions/swiftmailer/composer.json index 4c9e831..f248942 100644 --- a/extensions/swiftmailer/composer.json +++ b/extensions/swiftmailer/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-swiftmailer", "description": "The SwiftMailer integration for the Yii framework", - "keywords": ["yii", "swift", "swiftmailer", "mail", "email", "mailer"], + "keywords": ["yii2", "swift", "swiftmailer", "mail", "email", "mailer"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/extensions/twig/composer.json b/extensions/twig/composer.json index 1638058..ee7b0f8 100644 --- a/extensions/twig/composer.json +++ b/extensions/twig/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2-twig", "description": "The Twig integration for the Yii framework", - "keywords": ["yii", "twig", "renderer"], + "keywords": ["yii2", "twig", "renderer"], "type": "yii2-extension", "license": "BSD-3-Clause", "support": { diff --git a/framework/BaseYii.php b/framework/BaseYii.php index 9152afc..c70808e 100644 --- a/framework/BaseYii.php +++ b/framework/BaseYii.php @@ -356,7 +356,7 @@ class BaseYii $config = array_merge(static::$objectConfig[$class], $config); } - if (($n = func_num_args()) > 1) { + if (func_num_args() > 1) { /** @var \ReflectionClass $reflection */ if (isset($reflections[$class])) { $reflection = $reflections[$class]; diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 4ce41d2..06eb826 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -49,6 +49,9 @@ Yii Framework 2 Change Log - 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 #2559: Going back on browser history breaks GridView filtering with `Pjax` (tonydspaniard) +- Bug #2607: `yii message` tool wasn't updating `message` table (mitalcoi) +- Bug #2624: Html::textArea() should respect "name" option. (qiangxue) - 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) @@ -59,6 +62,7 @@ Yii Framework 2 Change Log - 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) @@ -67,10 +71,12 @@ Yii Framework 2 Change Log - Enh #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe) - Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue) - Enh #1437: Added ListView::viewParams (qiangxue) +- Enh #1467: Added support for organizing controllers in subdirectories (qiangxue) - Enh #1469: ActiveRecord::find() now works with default conditions (default scope) applied by createQuery (cebe) - Enh #1476: Add yii\web\Session::handler property (nineinchnick) - Enh #1499: Added `ActionColumn::controller` property to support customizing the controller for handling GridView actions (qiangxue) - Enh #1523: Query conditions now allow to use the NOT operator (cebe) +- Enh #1535: Improved `yii\web\User` to start session only when needed. Also prepared it for use without session. (qiangxue) - Enh #1562: Added `yii\bootstrap\Tabs::linkOptions` (kartik-v) - Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark) - Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue) @@ -118,12 +124,14 @@ Yii Framework 2 Change Log - 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) @@ -143,8 +151,14 @@ 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) +- Enh: LinkPager can now register relational link tags in the html header for prev, next, first and last page (cebe) +- Enh: Added `yii\web\UrlRuleInterface` and `yii\web\CompositeUrlRule` (qiangxue) +- Enh: Added `yii\web\Request::getAuthUser()` and `getAuthPassword()` (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 #1564: Removed `yii\web\Session::autoStart` and added `hasSessionId`. Session will be automatically started when accessing session data (qiangxue) - Chg #1586: `QueryBuilder::buildLikeCondition()` will now escape special characters and use percentage characters by default (qiangxue) - Chg #1610: `Html::activeCheckboxList()` and `Html::activeRadioList()` will submit an empty string if no checkbox/radio is selected (qiangxue) - Chg #1643: Added default value for `Captcha::options` (qiangxue) @@ -171,6 +185,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) @@ -202,7 +217,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) diff --git a/framework/assets/pjax/LICENSE b/framework/assets/pjax/LICENSE deleted file mode 100644 index 42c0317..0000000 --- a/framework/assets/pjax/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) Chris Wanstrath - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -Software), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/framework/assets/pjax/jquery.pjax.js b/framework/assets/pjax/jquery.pjax.js deleted file mode 100644 index 1934d80..0000000 --- a/framework/assets/pjax/jquery.pjax.js +++ /dev/null @@ -1,839 +0,0 @@ -// jquery.pjax.js -// copyright chris wanstrath -// https://github.com/defunkt/jquery-pjax - -(function($){ - -// When called on a container with a selector, fetches the href with -// ajax into the container or with the data-pjax attribute on the link -// itself. -// -// Tries to make sure the back button and ctrl+click work the way -// you'd expect. -// -// Exported as $.fn.pjax -// -// Accepts a jQuery ajax options object that may include these -// pjax specific options: -// -// -// container - Where to stick the response body. Usually a String selector. -// $(container).html(xhr.responseBody) -// (default: current jquery context) -// push - Whether to pushState the URL. Defaults to true (of course). -// replace - Want to use replaceState instead? That's cool. -// -// For convenience the second parameter can be either the container or -// the options object. -// -// Returns the jQuery object - function fnPjax(selector, container, options) { - var context = this - return this.on('click.pjax', selector, function(event) { - var opts = $.extend({}, optionsFor(container, options)) - if (!opts.container) - opts.container = $(this).attr('data-pjax') || context - handleClick(event, opts) - }) - } - -// Public: pjax on click handler -// -// Exported as $.pjax.click. -// -// event - "click" jQuery.Event -// options - pjax options -// -// Examples -// -// $(document).on('click', 'a', $.pjax.click) -// // is the same as -// $(document).pjax('a') -// -// $(document).on('click', 'a', function(event) { -// var container = $(this).closest('[data-pjax-container]') -// $.pjax.click(event, container) -// }) -// -// Returns nothing. - function handleClick(event, container, options) { - options = optionsFor(container, options) - - var link = event.currentTarget - - if (link.tagName.toUpperCase() !== 'A') - throw "$.fn.pjax or $.pjax.click requires an anchor element" - - // Middle click, cmd click, and ctrl click should open - // links in a new tab as normal. - if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) - return - - // Ignore cross origin links - if ( location.protocol !== link.protocol || location.hostname !== link.hostname ) - return - - // Ignore anchors on the same page - if (link.hash && link.href.replace(link.hash, '') === - location.href.replace(location.hash, '')) - return - - // Ignore empty anchor "foo.html#" - if (link.href === location.href + '#') - return - - var defaults = { - url: link.href, - container: $(link).attr('data-pjax'), - target: link - } - - var opts = $.extend({}, defaults, options) - var clickEvent = $.Event('pjax:click') - $(link).trigger(clickEvent, [opts]) - - if (!clickEvent.isDefaultPrevented()) { - pjax(opts) - event.preventDefault() - $(link).trigger('pjax:clicked', [opts]) - } - } - -// Public: pjax on form submit handler -// -// Exported as $.pjax.submit -// -// event - "click" jQuery.Event -// options - pjax options -// -// Examples -// -// $(document).on('submit', 'form', function(event) { -// var container = $(this).closest('[data-pjax-container]') -// $.pjax.submit(event, container) -// }) -// -// Returns nothing. - function handleSubmit(event, container, options) { - options = optionsFor(container, options) - - var form = event.currentTarget - - if (form.tagName.toUpperCase() !== 'FORM') - throw "$.pjax.submit requires a form element" - - var defaults = { - type: form.method.toUpperCase(), - url: form.action, - data: $(form).serializeArray(), - container: $(form).attr('data-pjax'), - target: form - } - - pjax($.extend({}, defaults, options)) - - event.preventDefault() - } - -// Loads a URL with ajax, puts the response body inside a container, -// then pushState()'s the loaded URL. -// -// Works just like $.ajax in that it accepts a jQuery ajax -// settings object (with keys like url, type, data, etc). -// -// Accepts these extra keys: -// -// container - Where to stick the response body. -// $(container).html(xhr.responseBody) -// push - Whether to pushState the URL. Defaults to true (of course). -// replace - Want to use replaceState instead? That's cool. -// -// Use it just like $.ajax: -// -// var xhr = $.pjax({ url: this.href, container: '#main' }) -// console.log( xhr.readyState ) -// -// Returns whatever $.ajax returns. - function pjax(options) { - options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options) - - if ($.isFunction(options.url)) { - options.url = options.url() - } - - var target = options.target - - var hash = parseURL(options.url).hash - - var context = options.context = findContainerFor(options.container) - - // We want the browser to maintain two separate internal caches: one - // for pjax'd partial page loads and one for normal page loads. - // Without adding this secret parameter, some browsers will often - // confuse the two. - if (!options.data) options.data = {} - options.data._pjax = context.selector - - function fire(type, args) { - var event = $.Event(type, { relatedTarget: target }) - context.trigger(event, args) - return !event.isDefaultPrevented() - } - - var timeoutTimer - - options.beforeSend = function(xhr, settings) { - // No timeout for non-GET requests - // Its not safe to request the resource again with a fallback method. - if (settings.type !== 'GET') { - settings.timeout = 0 - } - - xhr.setRequestHeader('X-PJAX', 'true') - xhr.setRequestHeader('X-PJAX-Container', context.selector) - - if (!fire('pjax:beforeSend', [xhr, settings])) - return false - - if (settings.timeout > 0) { - timeoutTimer = setTimeout(function() { - if (fire('pjax:timeout', [xhr, options])) - xhr.abort('timeout') - }, settings.timeout) - - // Clear timeout setting so jquerys internal timeout isn't invoked - settings.timeout = 0 - } - - options.requestUrl = parseURL(settings.url).href - } - - options.complete = function(xhr, textStatus) { - if (timeoutTimer) - clearTimeout(timeoutTimer) - - fire('pjax:complete', [xhr, textStatus, options]) - - fire('pjax:end', [xhr, options]) - } - - options.error = function(xhr, textStatus, errorThrown) { - var container = extractContainer("", xhr, options) - - var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options]) - if (options.type == 'GET' && textStatus !== 'abort' && allowed) { - locationReplace(container.url) - } - } - - options.success = function(data, status, xhr) { - // If $.pjax.defaults.version is a function, invoke it first. - // Otherwise it can be a static string. - var currentVersion = (typeof $.pjax.defaults.version === 'function') ? - $.pjax.defaults.version() : - $.pjax.defaults.version - - var latestVersion = xhr.getResponseHeader('X-PJAX-Version') - - var container = extractContainer(data, xhr, options) - - // If there is a layout version mismatch, hard load the new url - if (currentVersion && latestVersion && currentVersion !== latestVersion) { - locationReplace(container.url) - return - } - - // If the new response is missing a body, hard load the page - if (!container.contents) { - locationReplace(container.url) - return - } - - pjax.state = { - id: options.id || uniqueId(), - url: container.url, - title: container.title, - container: context.selector, - fragment: options.fragment, - timeout: options.timeout - } - - if (options.push || options.replace) { - window.history.replaceState(pjax.state, container.title, container.url) - } - - // Clear out any focused controls before inserting new page contents. - document.activeElement.blur() - - if (container.title) document.title = container.title - context.html(container.contents) - - // FF bug: Won't autofocus fields that are inserted via JS. - // This behavior is incorrect. So if theres no current focus, autofocus - // the last field. - // - // http://www.w3.org/html/wg/drafts/html/master/forms.html - var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0] - if (autofocusEl && document.activeElement !== autofocusEl) { - autofocusEl.focus(); - } - - executeScriptTags(container.scripts) - - // Scroll to top by default - if (typeof options.scrollTo === 'number') - $(window).scrollTop(options.scrollTo) - - // If the URL has a hash in it, make sure the browser - // knows to navigate to the hash. - if ( hash !== '' ) { - // Avoid using simple hash set here. Will add another history - // entry. Replace the url with replaceState and scroll to target - // by hand. - // - // window.location.hash = hash - var url = parseURL(container.url) - url.hash = hash - - pjax.state.url = url.href - window.history.replaceState(pjax.state, container.title, url.href) - - var target = $(url.hash) - if (target.length) $(window).scrollTop(target.offset().top) - } - - fire('pjax:success', [data, status, xhr, options]) - } - - - // Initialize pjax.state for the initial page load. Assume we're - // using the container and options of the link we're loading for the - // back button to the initial page. This ensures good back button - // behavior. - if (!pjax.state) { - pjax.state = { - id: uniqueId(), - url: window.location.href, - title: document.title, - container: context.selector, - fragment: options.fragment, - timeout: options.timeout - } - window.history.replaceState(pjax.state, document.title) - } - - // Cancel the current request if we're already pjaxing - var xhr = pjax.xhr - if ( xhr && xhr.readyState < 4) { - xhr.onreadystatechange = $.noop - xhr.abort() - } - - pjax.options = options - var xhr = pjax.xhr = $.ajax(options) - - if (xhr.readyState > 0) { - if (options.push && !options.replace) { - // Cache current container element before replacing it - cachePush(pjax.state.id, context.clone().contents()) - - window.history.pushState(null, "", stripPjaxParam(options.requestUrl)) - } - - fire('pjax:start', [xhr, options]) - fire('pjax:send', [xhr, options]) - } - - return pjax.xhr - } - -// Public: Reload current page with pjax. -// -// Returns whatever $.pjax returns. - function pjaxReload(container, options) { - var defaults = { - url: window.location.href, - push: false, - replace: true, - scrollTo: false - } - - return pjax($.extend(defaults, optionsFor(container, options))) - } - -// Internal: Hard replace current state with url. -// -// Work for around WebKit -// https://bugs.webkit.org/show_bug.cgi?id=93506 -// -// Returns nothing. - function locationReplace(url) { - window.history.replaceState(null, "", "#") - window.location.replace(url) - } - - - var initialPop = true - var initialURL = window.location.href - var initialState = window.history.state - -// Initialize $.pjax.state if possible -// Happens when reloading a page and coming forward from a different -// session history. - if (initialState && initialState.container) { - pjax.state = initialState - } - -// Non-webkit browsers don't fire an initial popstate event - if ('state' in window.history) { - initialPop = false - } - -// popstate handler takes care of the back and forward buttons -// -// You probably shouldn't use pjax on pages with other pushState -// stuff yet. - function onPjaxPopstate(event) { - var state = event.state - - if (state && state.container) { - // When coming forward from a separate history session, will get an - // initial pop with a state we are already at. Skip reloading the current - // page. - if (initialPop && initialURL == state.url) return - - // If popping back to the same state, just skip. - // Could be clicking back from hashchange rather than a pushState. - if (pjax.state.id === state.id) return - - var container = $(state.container) - if (container.length) { - var direction, contents = cacheMapping[state.id] - - if (pjax.state) { - // Since state ids always increase, we can deduce the history - // direction from the previous state. - direction = pjax.state.id < state.id ? 'forward' : 'back' - - // Cache current container before replacement and inform the - // cache which direction the history shifted. - cachePop(direction, pjax.state.id, container.clone().contents()) - } - - var popstateEvent = $.Event('pjax:popstate', { - state: state, - direction: direction - }) - container.trigger(popstateEvent) - - var options = { - id: state.id, - url: state.url, - container: container, - push: false, - fragment: state.fragment, - timeout: state.timeout, - scrollTo: false - } - - if (contents) { - container.trigger('pjax:start', [null, options]) - - if (state.title) document.title = state.title - container.html(contents) - pjax.state = state - - container.trigger('pjax:end', [null, options]) - } else { - pjax(options) - } - - // Force reflow/relayout before the browser tries to restore the - // scroll position. - container[0].offsetHeight - } else { - locationReplace(location.href) - } - } - initialPop = false - } - -// Fallback version of main pjax function for browsers that don't -// support pushState. -// -// Returns nothing since it retriggers a hard form submission. - function fallbackPjax(options) { - var url = $.isFunction(options.url) ? options.url() : options.url, - method = options.type ? options.type.toUpperCase() : 'GET' - - var form = $('<form>', { - method: method === 'GET' ? 'GET' : 'POST', - action: url, - style: 'display:none' - }) - - if (method !== 'GET' && method !== 'POST') { - form.append($('<input>', { - type: 'hidden', - name: '_method', - value: method.toLowerCase() - })) - } - - var data = options.data - if (typeof data === 'string') { - $.each(data.split('&'), function(index, value) { - var pair = value.split('=') - form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]})) - }) - } else if (typeof data === 'object') { - for (key in data) - form.append($('<input>', {type: 'hidden', name: key, value: data[key]})) - } - - $(document.body).append(form) - form.submit() - } - -// Internal: Generate unique id for state object. -// -// Use a timestamp instead of a counter since ids should still be -// unique across page loads. -// -// Returns Number. - function uniqueId() { - return (new Date).getTime() - } - -// Internal: Strips _pjax param from url -// -// url - String -// -// Returns String. - function stripPjaxParam(url) { - return url - .replace(/\?_pjax=[^&]+&?/, '?') - .replace(/_pjax=[^&]+&?/, '') - .replace(/[\?&]$/, '') - } - -// Internal: Parse URL components and returns a Locationish object. -// -// url - String URL -// -// Returns HTMLAnchorElement that acts like Location. - function parseURL(url) { - var a = document.createElement('a') - a.href = url - return a - } - -// Internal: Build options Object for arguments. -// -// For convenience the first parameter can be either the container or -// the options object. -// -// Examples -// -// optionsFor('#container') -// // => {container: '#container'} -// -// optionsFor('#container', {push: true}) -// // => {container: '#container', push: true} -// -// optionsFor({container: '#container', push: true}) -// // => {container: '#container', push: true} -// -// Returns options Object. - function optionsFor(container, options) { - // Both container and options - if ( container && options ) - options.container = container - - // First argument is options Object - else if ( $.isPlainObject(container) ) - options = container - - // Only container - else - options = {container: container} - - // Find and validate container - if (options.container) - options.container = findContainerFor(options.container) - - return options - } - -// Internal: Find container element for a variety of inputs. -// -// Because we can't persist elements using the history API, we must be -// able to find a String selector that will consistently find the Element. -// -// container - A selector String, jQuery object, or DOM Element. -// -// Returns a jQuery object whose context is `document` and has a selector. - function findContainerFor(container) { - container = $(container) - - if ( !container.length ) { - throw "no pjax container for " + container.selector - } else if ( container.selector !== '' && container.context === document ) { - return container - } else if ( container.attr('id') ) { - return $('#' + container.attr('id')) - } else { - throw "cant get selector for pjax container!" - } - } - -// Internal: Filter and find all elements matching the selector. -// -// Where $.fn.find only matches descendants, findAll will test all the -// top level elements in the jQuery object as well. -// -// elems - jQuery object of Elements -// selector - String selector to match -// -// Returns a jQuery object. - function findAll(elems, selector) { - return elems.filter(selector).add(elems.find(selector)); - } - - function parseHTML(html) { - return $.parseHTML(html, document, true) - } - -// Internal: Extracts container and metadata from response. -// -// 1. Extracts X-PJAX-URL header if set -// 2. Extracts inline <title> tags -// 3. Builds response Element and extracts fragment if set -// -// data - String response data -// xhr - XHR response -// options - pjax options Object -// -// Returns an Object with url, title, and contents keys. - function extractContainer(data, xhr, options) { - var obj = {} - - // Prefer X-PJAX-URL header if it was set, otherwise fallback to - // using the original requested url. - obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl) - - // Attempt to parse response html into elements - if (/<html/i.test(data)) { - var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0])) - var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0])) - } else { - var $head = $body = $(parseHTML(data)) - } - - // If response data is empty, return fast - if ($body.length === 0) - return obj - - // If there's a <title> tag in the header, use it as - // the page's title. - obj.title = findAll($head, 'title').last().text() - - if (options.fragment) { - // If they specified a fragment, look for it in the response - // and pull it out. - if (options.fragment === 'body') { - var $fragment = $body - } else { - var $fragment = findAll($body, options.fragment).first() - } - - if ($fragment.length) { - obj.contents = $fragment.contents() - - // If there's no title, look for data-title and title attributes - // on the fragment - if (!obj.title) - obj.title = $fragment.attr('title') || $fragment.data('title') - } - - } else if (!/<html/i.test(data)) { - obj.contents = $body - } - - // Clean up any <title> tags - if (obj.contents) { - // Remove any parent title elements - obj.contents = obj.contents.not(function() { return $(this).is('title') }) - - // Then scrub any titles from their descendants - obj.contents.find('title').remove() - - // Gather all script[src] elements - obj.scripts = findAll(obj.contents, 'script[src]').remove() - obj.contents = obj.contents.not(obj.scripts) - } - - // Trim any whitespace off the title - if (obj.title) obj.title = $.trim(obj.title) - - return obj - } - -// Load an execute scripts using standard script request. -// -// Avoids jQuery's traditional $.getScript which does a XHR request and -// globalEval. -// -// scripts - jQuery object of script Elements -// -// Returns nothing. - function executeScriptTags(scripts) { - if (!scripts) return - - var existingScripts = $('script[src]') - - scripts.each(function() { - var src = this.src - var matchedScripts = existingScripts.filter(function() { - return this.src === src - }) - if (matchedScripts.length) return - - var script = document.createElement('script') - script.type = $(this).attr('type') - script.src = $(this).attr('src') - document.head.appendChild(script) - }) - } - -// Internal: History DOM caching class. - var cacheMapping = {} - var cacheForwardStack = [] - var cacheBackStack = [] - -// Push previous state id and container contents into the history -// cache. Should be called in conjunction with `pushState` to save the -// previous container contents. -// -// id - State ID Number -// value - DOM Element to cache -// -// Returns nothing. - function cachePush(id, value) { - cacheMapping[id] = value - cacheBackStack.push(id) - - // Remove all entires in forward history stack after pushing - // a new page. - while (cacheForwardStack.length) - delete cacheMapping[cacheForwardStack.shift()] - - // Trim back history stack to max cache length. - while (cacheBackStack.length > pjax.defaults.maxCacheLength) - delete cacheMapping[cacheBackStack.shift()] - } - -// Shifts cache from directional history cache. Should be -// called on `popstate` with the previous state id and container -// contents. -// -// direction - "forward" or "back" String -// id - State ID Number -// value - DOM Element to cache -// -// Returns nothing. - function cachePop(direction, id, value) { - var pushStack, popStack - cacheMapping[id] = value - - if (direction === 'forward') { - pushStack = cacheBackStack - popStack = cacheForwardStack - } else { - pushStack = cacheForwardStack - popStack = cacheBackStack - } - - pushStack.push(id) - if (id = popStack.pop()) - delete cacheMapping[id] - } - -// Public: Find version identifier for the initial page load. -// -// Returns String version or undefined. - function findVersion() { - return $('meta').filter(function() { - var name = $(this).attr('http-equiv') - return name && name.toUpperCase() === 'X-PJAX-VERSION' - }).attr('content') - } - -// Install pjax functions on $.pjax to enable pushState behavior. -// -// Does nothing if already enabled. -// -// Examples -// -// $.pjax.enable() -// -// Returns nothing. - function enable() { - $.fn.pjax = fnPjax - $.pjax = pjax - $.pjax.enable = $.noop - $.pjax.disable = disable - $.pjax.click = handleClick - $.pjax.submit = handleSubmit - $.pjax.reload = pjaxReload - $.pjax.defaults = { - timeout: 650, - push: true, - replace: false, - type: 'GET', - dataType: 'html', - scrollTo: 0, - maxCacheLength: 20, - version: findVersion - } - $(window).on('popstate.pjax', onPjaxPopstate) - } - -// Disable pushState behavior. -// -// This is the case when a browser doesn't support pushState. It is -// sometimes useful to disable pushState for debugging on a modern -// browser. -// -// Examples -// -// $.pjax.disable() -// -// Returns nothing. - function disable() { - $.fn.pjax = function() { return this } - $.pjax = fallbackPjax - $.pjax.enable = enable - $.pjax.disable = $.noop - $.pjax.click = $.noop - $.pjax.submit = $.noop - $.pjax.reload = function() { window.location.reload() } - - $(window).off('popstate.pjax', onPjaxPopstate) - } - - -// Add the state property to jQuery's event object so we can use it in -// $(window).bind('popstate') - if ( $.inArray('state', $.event.props) < 0 ) - $.event.props.push('state') - -// Is pjax supported by this browser? - $.support.pjax = - window.history && window.history.pushState && window.history.replaceState && - // pushState isn't reliable on iOS until 5. - !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/) - - $.support.pjax ? enable() : disable() - -})(jQuery); diff --git a/framework/assets/yii.gridView.js b/framework/assets/yii.gridView.js index 496e3cd..ca2b99a 100644 --- a/framework/assets/yii.gridView.js +++ b/framework/assets/yii.gridView.js @@ -26,17 +26,18 @@ filterSelector: undefined }; + var gridData = {}; + var methods = { init: function (options) { return this.each(function () { var $e = $(this); var settings = $.extend({}, defaults, options || {}); - $e.data('yiiGridView', { - settings: settings - }); + gridData[$e.prop('id')] = {settings: settings}; var enterPressed = false; - $(settings.filterSelector).on('change.yiiGridView keydown.yiiGridView', function (event) { + $(document).off('change.yiiGridView keydown.yiiGridView', settings.filterSelector) + .on('change.yiiGridView keydown.yiiGridView', settings.filterSelector, function (event) { if (event.type === 'keydown') { if (event.keyCode !== 13) { return; // only react to enter key @@ -60,7 +61,7 @@ applyFilter: function () { var $grid = $(this); - var settings = $grid.data('yiiGridView').settings; + var settings = gridData[$grid.prop('id')].settings; var data = {}; $.each($(settings.filterSelector).serializeArray(), function () { data[this.name] = this.value; @@ -85,15 +86,16 @@ setSelectionColumn: function (options) { var $grid = $(this); - var data = $grid.data('yiiGridView'); - data.selectionColumn = options.name; + var id = $(this).prop('id'); + gridData[id].selectionColumn = options.name; if (!options.multiple) { return; } - $grid.on('click.yiiGridView', "input[name='" + options.checkAll + "']", function () { + var inputs = "#" + id + " input[name='" + options.checkAll + "']"; + $(document).off('click.yiiGridView', inputs).on('click.yiiGridView', inputs, function () { $grid.find("input[name='" + options.name + "']:enabled").prop('checked', this.checked); }); - $grid.on('click.yiiGridView', "input[name='" + options.name + "']:enabled", function () { + $(document).off('click.yiiGridView', inputs + ":enabled").on('click.yiiGridView', inputs + ":enabled", function () { var all = $grid.find("input[name='" + options.name + "']").length == $grid.find("input[name='" + options.name + "']:checked").length; $grid.find("input[name='" + options.checkAll + "']").prop('checked', all); }); @@ -101,7 +103,7 @@ getSelectedRows: function () { var $grid = $(this); - var data = $grid.data('yiiGridView'); + var data = gridData[$grid.prop('id')]; var keys = []; if (data.selectionColumn) { $grid.find("input[name='" + data.selectionColumn + "']:checked").each(function () { @@ -118,8 +120,9 @@ }); }, - data: function() { - return this.data('yiiGridView'); + data: function () { + var id = $(this).prop('id'); + return gridData[id]; } }; })(window.jQuery); diff --git a/framework/base/Action.php b/framework/base/Action.php index ab80832..fd9c361 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -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; + } } /** diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php index 3b8f8eb..6fb114c 100644 --- a/framework/base/ActionFilter.php +++ b/framework/base/ActionFilter.php @@ -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; } /** diff --git a/framework/base/ArrayableTrait.php b/framework/base/ArrayableTrait.php new file mode 100644 index 0000000..f5bc9d6 --- /dev/null +++ b/framework/base/ArrayableTrait.php @@ -0,0 +1,163 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\base; + +use Yii; +use yii\helpers\ArrayHelper; +use yii\web\Link; +use yii\web\Linkable; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +trait ArrayableTrait +{ + /** + * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified. + * + * A field is a named element in the returned array by [[toArray()]]. + * + * This method should return an array of field names or field definitions. + * If the former, the field name will be treated as an object property name whose value will be used + * as the field value. If the latter, the array key should be the field name while the array value should be + * the corresponding field definition which can be either an object property name or a PHP callable + * returning the corresponding field value. The signature of the callable should be: + * + * ```php + * function ($field, $model) { + * // return field value + * } + * ``` + * + * For example, the following code declares four fields: + * + * - `email`: the field name is the same as the property name `email`; + * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their + * values are obtained from the `first_name` and `last_name` properties; + * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name` + * and `last_name`. + * + * ```php + * return [ + * 'email', + * 'firstName' => 'first_name', + * 'lastName' => 'last_name', + * 'fullName' => function () { + * return $this->first_name . ' ' . $this->last_name; + * }, + * ]; + * ``` + * + * In this method, you may also want to return different lists of fields based on some context + * information. For example, depending on the privilege of the current application user, + * you may return different sets of visible fields or filter out some fields. + * + * The default implementation of this method returns the public object member variables. + * + * @return array the list of field names or field definitions. + * @see toArray() + */ + public function fields() + { + $fields = array_keys(Yii::getObjectVars($this)); + return array_combine($fields, $fields); + } + + /** + * Returns the list of fields that can be expanded further and returned by [[toArray()]]. + * + * This method is similar to [[fields()]] except that the list of fields returned + * by this method are not returned by default by [[toArray()]]. Only when field names + * to be expanded are explicitly specified when calling [[toArray()]], will their values + * be exported. + * + * The default implementation returns an empty array. + * + * You may override this method to return a list of expandable fields based on some context information + * (e.g. the current application user). + * + * @return array the list of expandable field names or field definitions. Please refer + * to [[fields()]] on the format of the return value. + * @see toArray() + * @see fields() + */ + public function extraFields() + { + return []; + } + + /** + * Converts the model into an array. + * + * This method will first identify which fields to be included in the resulting array by calling [[resolveFields()]]. + * It will then turn the model into an array with these fields. If `$recursive` is true, + * any embedded objects will also be converted into arrays. + * + * If the model implements the [[Linkable]] interface, the resulting array will also have a `_link` element + * which refers to a list of links as specified by the interface. + * + * @param array $fields the fields being requested. If empty, all fields as specified by [[fields()]] will be returned. + * @param array $expand the additional fields being requested for exporting. Only fields declared in [[extraFields()]] + * will be considered. + * @param boolean $recursive whether to recursively return array representation of embedded objects. + * @return array the array representation of the object + */ + public function toArray(array $fields = [], array $expand = [], $recursive = true) + { + $data = []; + foreach ($this->resolveFields($fields, $expand) as $field => $definition) { + $data[$field] = is_string($definition) ? $this->$definition : call_user_func($definition, $field, $this); + } + + if ($this instanceof Linkable) { + $data['_links'] = Link::serialize($this->getLinks()); + } + + return $recursive ? ArrayHelper::toArray($data) : $data; + } + + /** + * Determines which fields can be returned by [[toArray()]]. + * This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]] + * to determine which fields can be returned. + * @param array $fields the fields being requested for exporting + * @param array $expand the additional fields being requested for exporting + * @return array the list of fields to be exported. The array keys are the field names, and the array values + * are the corresponding object property names or PHP callables returning the field values. + */ + protected function resolveFields(array $fields, array $expand) + { + $result = []; + + foreach ($this->fields() as $field => $definition) { + if (is_integer($field)) { + $field = $definition; + } + if (empty($fields) || in_array($field, $fields, true)) { + $result[$field] = $definition; + } + } + + if (empty($expand)) { + return $result; + } + + foreach ($this->extraFields() as $field => $definition) { + if (is_integer($field)) { + $field = $definition; + } + if (in_array($field, $expand, true)) { + $result[$field] = $definition; + } + } + + return $result; + } +} diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 333c99a..27d2345 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -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; } /** diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index bf1faad..606a795 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -93,10 +93,9 @@ class ErrorHandler extends Component return; } - $useErrorView = !YII_DEBUG || $exception instanceof UserException; - $response = Yii::$app->getResponse(); - $response->getHeaders()->removeAll(); + + $useErrorView = $response->format === \yii\web\Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException); if ($useErrorView && $this->errorAction !== null) { $result = Yii::$app->runAction($this->errorAction); @@ -121,7 +120,7 @@ class ErrorHandler extends Component ]); } } elseif ($exception instanceof Arrayable) { - $response->data = $exception; + $response->data = $exception->toArray(); } else { $response->data = [ 'type' => get_class($exception), diff --git a/framework/base/Formatter.php b/framework/base/Formatter.php index 7214cb5..80c08e1 100644 --- a/framework/base/Formatter.php +++ b/framework/base/Formatter.php @@ -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"); @@ -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); + } + } } diff --git a/framework/base/Model.php b/framework/base/Model.php index 8ec6e40..8159bb9 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -13,9 +13,12 @@ use ArrayObject; use ArrayIterator; use ReflectionClass; use IteratorAggregate; +use yii\helpers\ArrayHelper; use yii\helpers\Inflector; use yii\validators\RequiredValidator; use yii\validators\Validator; +use yii\web\Link; +use yii\web\Linkable; /** * Model is the base class for data models. @@ -54,11 +57,12 @@ use yii\validators\Validator; */ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable { + use ArrayableTrait; + /** * The name of the default scenario. */ const SCENARIO_DEFAULT = 'default'; - /** * @event ModelEvent an event raised at the beginning of [[validate()]]. You may set * [[ModelEvent::isValid]] to be false to stop the validation. @@ -516,7 +520,8 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab /** * Returns the first error of every attribute in the model. - * @return array the first errors. An empty array will be returned if there is no error. + * @return array the first errors. The array keys are the attribute names, and the array + * values are the corresponding error messages. An empty array will be returned if there is no error. * @see getErrors() * @see getFirstError() */ @@ -526,13 +531,13 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab return []; } else { $errors = []; - foreach ($this->_errors as $attributeErrors) { - if (isset($attributeErrors[0])) { - $errors[] = $attributeErrors[0]; + foreach ($this->_errors as $name => $es) { + if (!empty($es)) { + $errors[$name] = reset($es); } } + return $errors; } - return $errors; } /** @@ -789,13 +794,92 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab } /** - * Converts the object into an array. - * The default implementation will return [[attributes]]. - * @return array the array representation of the object + * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified. + * + * A field is a named element in the returned array by [[toArray()]]. + * + * This method should return an array of field names or field definitions. + * If the former, the field name will be treated as an object property name whose value will be used + * as the field value. If the latter, the array key should be the field name while the array value should be + * the corresponding field definition which can be either an object property name or a PHP callable + * returning the corresponding field value. The signature of the callable should be: + * + * ```php + * function ($field, $model) { + * // return field value + * } + * ``` + * + * For example, the following code declares four fields: + * + * - `email`: the field name is the same as the property name `email`; + * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their + * values are obtained from the `first_name` and `last_name` properties; + * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name` + * and `last_name`. + * + * ```php + * return [ + * 'email', + * 'firstName' => 'first_name', + * 'lastName' => 'last_name', + * 'fullName' => function () { + * return $this->first_name . ' ' . $this->last_name; + * }, + * ]; + * ``` + * + * In this method, you may also want to return different lists of fields based on some context + * information. For example, depending on [[scenario]] or the privilege of the current application user, + * you may return different sets of visible fields or filter out some fields. + * + * The default implementation of this method returns [[attributes()]] indexed by the same attribute names. + * + * @return array the list of field names or field definitions. + * @see toArray() + */ + public function fields() + { + $fields = $this->attributes(); + return array_combine($fields, $fields); + } + + /** + * Determines which fields can be returned by [[toArray()]]. + * This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]] + * to determine which fields can be returned. + * @param array $fields the fields being requested for exporting + * @param array $expand the additional fields being requested for exporting + * @return array the list of fields to be exported. The array keys are the field names, and the array values + * are the corresponding object property names or PHP callables returning the field values. */ - public function toArray() + protected function resolveFields(array $fields, array $expand) { - return $this->getAttributes(); + $result = []; + + foreach ($this->fields() as $field => $definition) { + if (is_integer($field)) { + $field = $definition; + } + if (empty($fields) || in_array($field, $fields, true)) { + $result[$field] = $definition; + } + } + + if (empty($expand)) { + return $result; + } + + foreach ($this->extraFields() as $field => $definition) { + if (is_integer($field)) { + $field = $definition; + } + if (in_array($field, $expand, true)) { + $result[$field] = $definition; + } + } + + return $result; } /** diff --git a/framework/base/Module.php b/framework/base/Module.php index b65ad8b..cba919d 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -359,6 +359,9 @@ class Module extends Component return $this->_modules[$id]; } elseif ($load) { Yii::trace("Loading module: $id", __METHOD__); + if (is_array($this->_modules[$id]) && !isset($this->_modules[$id]['class'])) { + $this->_modules[$id]['class'] = 'yii\base\Module'; + } return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); } } @@ -593,12 +596,21 @@ class Module extends Component } /** - * Creates a controller instance based on the controller ID. + * Creates a controller instance based on the given route. + * + * The route should be relative to this module. The method implements the following algorithm + * to resolve the given route: * - * The controller is created within this module. The method first attempts to - * create the controller based on the [[controllerMap]] of the module. If not available, - * it will look for the controller class under the [[controllerPath]] and create an - * instance of it. + * 1. If the route is empty, use [[defaultRoute]]; + * 2. If the first segment of the route is a valid module ID as declared in [[modules]], + * call the module's `createController()` with the rest part of the route; + * 3. If the first segment of the route is found in [[controllerMap]], create a controller + * based on the corresponding configuration found in [[controllerMap]]; + * 4. The given route is in the format of `abc/def/xyz`. Try either `abc\DefController` + * or `abc\def\XyzController` class within the [[controllerNamespace|controller namespace]]. + * + * If any of the above steps resolves into a controller, it is returned together with the rest + * part of the route which will be treated as the action ID. Otherwise, false will be returned. * * @param string $route the route consisting of module, controller and action IDs. * @return array|boolean If the controller is created successfully, it will be returned together @@ -610,6 +622,13 @@ class Module extends Component if ($route === '') { $route = $this->defaultRoute; } + + // double slashes or leading/ending slashes may cause substr problem + $route = trim($route, '/'); + if (strpos($route, '//') !== false) { + return false; + } + if (strpos($route, '/') !== false) { list ($id, $route) = explode('/', $route, 2); } else { @@ -617,29 +636,73 @@ class Module extends Component $route = ''; } + // module and controller map take precedence $module = $this->getModule($id); if ($module !== null) { return $module->createController($route); } - if (isset($this->controllerMap[$id])) { $controller = Yii::createObject($this->controllerMap[$id], $id, $this); - } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id) && strpos($id, '--') === false && trim($id, '-') === $id) { - $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $id))) . 'Controller'; - $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; - if (!is_file($classFile)) { - return false; - } - $className = ltrim($this->controllerNamespace . '\\' . $className, '\\'); - Yii::$classMap[$className] = $classFile; - if (is_subclass_of($className, 'yii\base\Controller')) { - $controller = new $className($id, $this); - } elseif (YII_DEBUG) { - throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); - } + return [$controller, $route]; + } + + if (($pos = strrpos($route, '/')) !== false) { + $id .= '/' . substr($route, 0, $pos); + $route = substr($route, $pos + 1); + } + + $controller = $this->createControllerByID($id); + if ($controller === null && $route !== '') { + $controller = $this->createControllerByID($id . '/' . $route); + $route = ''; } - return isset($controller) ? [$controller, $route] : false; + return $controller === null ? false : [$controller, $route]; + } + + /** + * Creates a controller based on the given controller ID. + * + * The controller ID is relative to this module. The controller class + * should be located under [[controllerPath]] and namespaced under [[controllerNamespace]]. + * + * Note that this method does not check [[modules]] or [[controllerMap]]. + * + * @param string $id the controller ID + * @return Controller the newly created controller instance, or null if the controller ID is invalid. + * @throws InvalidConfigException if the controller class and its file name do not match. + * This exception is only thrown when in debug mode. + */ + public function createControllerByID($id) + { + if (!preg_match('%^[a-z0-9\\-_/]+$%', $id)) { + return null; + } + + $pos = strrpos($id, '/'); + if ($pos === false) { + $prefix = ''; + $className = $id; + } else { + $prefix = substr($id, 0, $pos + 1); + $className = substr($id, $pos + 1); + } + + $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $className))) . 'Controller'; + $classFile = $this->controllerPath . '/' . $prefix . $className . '.php'; + $className = ltrim($this->controllerNamespace . '\\' . str_replace('/', '\\', $prefix) . $className, '\\'); + if (strpos($className, '-') !== false || !is_file($classFile)) { + return null; + } + + Yii::$classMap[$className] = $classFile; + if (is_subclass_of($className, 'yii\base\Controller')) { + return new $className($id, $this); + } elseif (YII_DEBUG) { + throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); + } else { + return null; + } } /** @@ -658,10 +721,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; } } diff --git a/framework/base/Request.php b/framework/base/Request.php index b76886e..eb9f805 100644 --- a/framework/base/Request.php +++ b/framework/base/Request.php @@ -6,6 +6,7 @@ */ namespace yii\base; + use Yii; /** diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 01a6964..63382ad 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -13,7 +13,7 @@ use yii\helpers\FileHelper; /** * Theme represents an application theme. * - * When [[View]] renders a view file, it will check the [[Application::theme|active theme]] + * When [[View]] renders a view file, it will check the [[View::theme|active theme]] * to see if there is a themed version of the view file exists. If so, the themed version will be rendered instead. * * A theme is a directory consisting of view files which are meant to replace their non-themed counterparts. diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 11afde3..200cdde 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -6,6 +6,7 @@ */ namespace yii\caching; + use yii\base\InvalidConfigException; /** diff --git a/framework/caching/GroupDependency.php b/framework/caching/GroupDependency.php index 1cf7869..bcac858 100644 --- a/framework/caching/GroupDependency.php +++ b/framework/caching/GroupDependency.php @@ -6,6 +6,7 @@ */ namespace yii\caching; + use yii\base\InvalidConfigException; /** diff --git a/framework/composer.json b/framework/composer.json index bd11fc7..5a34ab0 100644 --- a/framework/composer.json +++ b/framework/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/yii2", "description": "Yii PHP Framework Version 2", - "keywords": ["yii", "framework"], + "keywords": ["yii2", "framework"], "homepage": "http://www.yiiframework.com/", "type": "library", "license": "BSD-3-Clause", @@ -54,6 +54,7 @@ "lib-pcre": "*", "yiisoft/yii2-composer": "*", "yiisoft/jquery": "~2.0 | ~1.10", + "yiisoft/jquery-pjax": "*", "ezyang/htmlpurifier": "4.6.*", "cebe/markdown": "0.9.*" }, diff --git a/framework/console/controllers/FixtureController.php b/framework/console/controllers/FixtureController.php index 858f08f..c72efd2 100644 --- a/framework/console/controllers/FixtureController.php +++ b/framework/console/controllers/FixtureController.php @@ -64,7 +64,7 @@ class FixtureController extends Controller public function globalOptions() { return array_merge(parent::globalOptions(), [ - 'namespace','globalFixtures' + 'namespace', 'globalFixtures' ]); } @@ -74,6 +74,7 @@ class FixtureController extends Controller * whitespace between names. Note that if you are loading fixtures to storage, for example: database or nosql, * storage will not be cleared, data will be appended to already existed. * @param array $fixtures + * @param array $except * @throws \yii\console\Exception */ public function actionLoad(array $fixtures, array $except = []) @@ -99,7 +100,7 @@ class FixtureController extends Controller } $filtered = array_diff($foundFixtures, $except); - $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures ,$filtered)); + $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $filtered)); if (!$fixtures) { throw new Exception('No fixtures were found in namespace: "' . $this->namespace . '"' . ''); @@ -317,5 +318,4 @@ class FixtureController extends Controller { return Yii::getAlias('@' . str_replace('\\', '/', $this->namespace)); } - } diff --git a/framework/console/controllers/MessageController.php b/framework/console/controllers/MessageController.php index 0345b69..cb34a38 100644 --- a/framework/console/controllers/MessageController.php +++ b/framework/console/controllers/MessageController.php @@ -134,11 +134,14 @@ class MessageController extends Controller throw new Exception('The "db" option must refer to a valid database application component.'); } $sourceMessageTable = isset($config['sourceMessageTable']) ? $config['sourceMessageTable'] : '{{%source_message}}'; + $messageTable = isset($config['messageTable']) ? $config['messageTable'] : '{{%message}}'; $this->saveMessagesToDb( $messages, $db, $sourceMessageTable, - $config['removeUnused'] + $messageTable, + $config['removeUnused'], + $config['languages'] ); } } @@ -149,9 +152,11 @@ class MessageController extends Controller * @param array $messages * @param \yii\db\Connection $db * @param string $sourceMessageTable + * @param string $messageTable * @param boolean $removeUnused + * @param array $languages */ - protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $removeUnused) + protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $messageTable, $removeUnused, $languages) { $q = new \yii\db\Query; $current = []; @@ -190,12 +195,17 @@ class MessageController extends Controller echo "Inserting new messages..."; $savedFlag = false; - foreach ($new as $category => $msgs) { + foreach ($new as $category => $msgs) { foreach ($msgs as $m) { $savedFlag = true; $db->createCommand() - ->insert($sourceMessageTable, ['category' => $category, 'message' => $m])->execute(); + ->insert($sourceMessageTable, ['category' => $category, 'message' => $m])->execute(); + $lastId = $db->getLastInsertID(); + foreach ($languages as $language) { + $db->createCommand() + ->insert($messageTable, ['id' => $lastId, 'language' => $language])->execute(); + } } } @@ -207,15 +217,20 @@ class MessageController extends Controller } else { if ($removeUnused) { $db->createCommand() - ->delete($sourceMessageTable, ['in', 'id', $obsolete])->execute(); - echo "deleted.\n"; + ->delete($sourceMessageTable, ['in', 'id', $obsolete])->execute(); + echo "deleted.\n"; } else { + $last_id = $db->getLastInsertID(); $db->createCommand() - ->update( + ->update( $sourceMessageTable, ['message' => new \yii\db\Expression("CONCAT('@@',message,'@@')")], ['in', 'id', $obsolete] )->execute(); + foreach ($languages as $language) { + $db->createCommand() + ->insert($messageTable, ['id' => $last_id, 'language' => $language])->execute(); + } echo "updated.\n"; } } @@ -268,7 +283,7 @@ class MessageController extends Controller { echo "Saving messages to $fileName..."; if (is_file($fileName)) { - if($format === 'po'){ + if ($format === 'po') { $translated = file_get_contents($fileName); preg_match_all('/(?<=msgid ").*(?="\n(#*)msgstr)/', $translated, $keys); preg_match_all('/(?<=msgstr ").*(?="\n\n)/', $translated, $values); @@ -285,7 +300,7 @@ class MessageController extends Controller $merged = []; $untranslated = []; foreach ($messages as $message) { - if($format === 'po'){ + if ($format === 'po') { $message = preg_replace('/\"/', '\"', $message); } if (array_key_exists($message, $translated) && strlen($translated[$message]) > 0) { @@ -317,9 +332,9 @@ class MessageController extends Controller if (false === $overwrite) { $fileName .= '.merged'; } - if ($format === 'po'){ + if ($format === 'po') { $output = ''; - foreach ($merged as $k => $v){ + foreach ($merged as $k => $v) { $k = preg_replace('/(\")|(\\\")/', "\\\"", $k); $v = preg_replace('/(\")|(\\\")/', "\\\"", $v); if (substr($v, 0, 2) === '@@' && substr($v, -2) === '@@') { @@ -338,7 +353,7 @@ class MessageController extends Controller if ($format === 'po') { $merged = ''; sort($messages); - foreach($messages as $message) { + foreach ($messages as $message) { $message = preg_replace('/(\")|(\\\")/', '\\\"', $message); $merged .= "msgid \"$message\"\n"; $merged .= "msgstr \"\"\n"; diff --git a/framework/data/BaseDataProvider.php b/framework/data/BaseDataProvider.php index e001e86..31acc2d 100644 --- a/framework/data/BaseDataProvider.php +++ b/framework/data/BaseDataProvider.php @@ -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) { diff --git a/framework/data/Pagination.php b/framework/data/Pagination.php index 9e9359c..29bedb7 100644 --- a/framework/data/Pagination.php +++ b/framework/data/Pagination.php @@ -8,7 +8,10 @@ namespace yii\data; use Yii; +use yii\base\Arrayable; use yii\base\Object; +use yii\web\Link; +use yii\web\Linkable; use yii\web\Request; /** @@ -65,14 +68,24 @@ use yii\web\Request; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class Pagination extends Object +class Pagination extends Object implements Linkable, Arrayable { + 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 +101,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 +119,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 +144,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 +163,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 +172,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, $validatePage = false) + { + 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 setPage($value) + public function setPageSize($value, $validatePageSize = false) { - $this->_page = $value; + 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 +256,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 +277,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 +288,60 @@ 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 = [ + Link::REL_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; + } + + /** + * @inheritdoc + */ + public function toArray() + { + return [ + 'totalCount' => $this->totalCount, + 'pageCount' => $this->getPageCount(), + 'currentPage' => $this->getPage(), + 'perPage' => $this->getPageSize(), + ]; + } + + /** + * 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; } } diff --git a/framework/db/ActiveQuery.php b/framework/db/ActiveQuery.php index 7fcb5e0..23e7f1f 100644 --- a/framework/db/ActiveQuery.php +++ b/framework/db/ActiveQuery.php @@ -86,6 +86,10 @@ class ActiveQuery extends Query implements ActiveQueryInterface * @see onCondition() */ public $on; + /** + * @var array a list of relations that this query should be joined with + */ + public $joinWith; /** @@ -116,7 +120,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface $this->findWith($this->with, $models); } if (!$this->asArray) { - foreach($models as $model) { + foreach ($models as $model) { $model->afterFind(); } } @@ -232,6 +236,10 @@ class ActiveQuery extends Query implements ActiveQueryInterface */ protected function createCommandInternal($db) { + if (!empty($this->joinWith)) { + $this->buildJoinWith(); + } + /** @var ActiveRecord $modelClass */ $modelClass = $this->modelClass; if ($db === null) { @@ -330,24 +338,32 @@ class ActiveQuery extends Query implements ActiveQueryInterface */ public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN') { - $with = (array)$with; - $this->joinWithRelations(new $this->modelClass, $with, $joinType); + $this->joinWith[] = [(array)$with, $eagerLoading, $joinType]; + return $this; + } - if (is_array($eagerLoading)) { - foreach ($with as $name => $callback) { - if (is_integer($name)) { - if (!in_array($callback, $eagerLoading, true)) { + private function buildJoinWith() + { + foreach ($this->joinWith as $config) { + list ($with, $eagerLoading, $joinType) = $config; + $this->joinWithRelations(new $this->modelClass, $with, $joinType); + + if (is_array($eagerLoading)) { + foreach ($with as $name => $callback) { + if (is_integer($name)) { + if (!in_array($callback, $eagerLoading, true)) { + unset($with[$name]); + } + } elseif (!in_array($name, $eagerLoading, true)) { unset($with[$name]); } - } elseif (!in_array($name, $eagerLoading, true)) { - unset($with[$name]); } + } elseif (!$eagerLoading) { + $with = []; } - } elseif (!$eagerLoading) { - $with = []; - } - return $this->with($with); + $this->with($with); + } } /** diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index e7461c6..539508b 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -35,9 +35,6 @@ use yii\helpers\StringHelper; * * class Customer extends \yii\db\ActiveRecord * { - * /** - * * @return string the name of the table associated with this ActiveRecord class. - * * / * public static function tableName() * { * return 'tbl_customer'; diff --git a/framework/db/ActiveRecordInterface.php b/framework/db/ActiveRecordInterface.php index 47cdb75..737abbf 100644 --- a/framework/db/ActiveRecordInterface.php +++ b/framework/db/ActiveRecordInterface.php @@ -87,6 +87,13 @@ interface ActiveRecordInterface public function getOldPrimaryKey($asArray = false); /** + * Returns a value indicating whether the given set of attributes represents the primary key for this model + * @param array $keys the set of attributes to check + * @return boolean whether the given set of attributes represents the primary key for this model + */ + public static function isPrimaryKey($keys); + + /** * Creates an [[ActiveQueryInterface|ActiveQuery]] instance for query purpose. * * This method is usually ment to be used like this: diff --git a/framework/db/ActiveRelationTrait.php b/framework/db/ActiveRelationTrait.php index 91df098..32f6953 100644 --- a/framework/db/ActiveRelationTrait.php +++ b/framework/db/ActiveRelationTrait.php @@ -136,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) { @@ -288,7 +288,7 @@ trait ActiveRelationTrait foreach ($primaryModels as $i => $primaryModel) { if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) { $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel); - } elseif (!empty($primaryModels[$i][$primaryName])) { + } elseif (!empty($primaryModels[$i][$primaryName])) { $primaryModels[$i][$primaryName][$name] = $primaryModel; } } diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index 2f4da5c..69bc1da 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -19,7 +19,7 @@ use yii\base\InvalidCallException; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. * - * @include @yii/db/ActiveRecord.md + * See [[yii\db\ActiveRecord]] for a concrete implementation. * * @property array $dirtyAttributes The changed attribute values (name-value pairs). This property is * read-only. @@ -344,7 +344,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface */ public function hasOne($class, $link) { - /** @var ActiveRecord $class */ + /** @var ActiveRecordInterface $class */ return $class::createQuery([ 'modelClass' => $class, 'primaryModel' => $this, @@ -385,7 +385,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface */ public function hasMany($class, $link) { - /** @var ActiveRecord $class */ + /** @var ActiveRecordInterface $class */ return $class::createQuery([ 'modelClass' => $class, 'primaryModel' => $this, @@ -398,7 +398,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface * Populates the named relation with the related records. * Note that this method does not check if the relation exists or not. * @param string $name the relation name (case-sensitive) - * @param ActiveRecord|array|null $records the related records to be populated into the relation. + * @param ActiveRecordInterface|array|null $records the related records to be populated into the relation. */ public function populateRelation($name, $records) { @@ -938,7 +938,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface * Returns a value indicating whether the given active record is the same as the current one. * The comparison is made by comparing the table names and the primary key values of the two active records. * If one of the records [[isNewRecord|is new]] they are also considered not equal. - * @param ActiveRecord $record record to compare to + * @param ActiveRecordInterface $record record to compare to * @return boolean whether the two active records refer to the same row in the same database table. */ public function equals($record) @@ -1106,7 +1106,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface * Note that this method requires that the primary key value is not null. * * @param string $name the case sensitive name of the relationship - * @param ActiveRecord $model the model to be linked with the current one. + * @param ActiveRecordInterface $model the model to be linked with the current one. * @param array $extraColumns additional column values to be saved into the pivot table. * This parameter is only meaningful for a relationship involving a pivot table * (i.e., a relation set with [[ActiveRelationTrait::via()]] or `[[ActiveQuery::viaTable()]]`.) @@ -1141,8 +1141,8 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface $columns[$k] = $v; } if (is_array($relation->via)) { - /** @var $viaClass ActiveRecord */ - /** @var $record ActiveRecord */ + /** @var $viaClass ActiveRecordInterface */ + /** @var $record ActiveRecordInterface */ $record = new $viaClass(); foreach ($columns as $column => $value) { $record->$column = $value; @@ -1193,7 +1193,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface * Otherwise, the foreign key will be set null and the model will be saved without validation. * * @param string $name the case sensitive name of the relationship. - * @param ActiveRecord $model the model to be unlinked from the current one. + * @param ActiveRecordInterface $model the model to be unlinked from the current one. * @param boolean $delete whether to delete the model that contains the foreign key. * If false, the model's foreign key will be set null and saved. * If true, the model containing the foreign key will be deleted. @@ -1221,7 +1221,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface $columns[$b] = $model->$a; } if (is_array($relation->via)) { - /** @var $viaClass ActiveRecord */ + /** @var $viaClass ActiveRecordInterface */ if ($delete) { $viaClass::deleteAll($columns); } else { @@ -1233,6 +1233,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface } } else { /** @var $viaTable string */ + /** @var Command $command */ $command = static::getDb()->createCommand(); if ($delete) { $command->delete($viaTable, $columns)->execute(); @@ -1265,7 +1266,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface if (!$relation->multiple) { unset($this->_related[$name]); } elseif (isset($this->_related[$name])) { - /** @var ActiveRecord $b */ + /** @var ActiveRecordInterface $b */ foreach ($this->_related[$name] as $a => $b) { if ($model->getPrimaryKey() == $b->getPrimaryKey()) { unset($this->_related[$name][$a]); @@ -1346,4 +1347,26 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface return $this->generateAttributeLabel($attribute); } + + /** + * @inheritdoc + * + * The default implementation returns the names of the columns whose values have been populated into this record. + */ + public function fields() + { + $fields = array_keys($this->_attributes); + return array_combine($fields, $fields); + } + + /** + * @inheritdoc + * + * The default implementation returns the names of the relations that have been populated into this record. + */ + public function extraFields() + { + $fields = array_keys($this->getRelatedRecords()); + return array_combine($fields, $fields); + } } diff --git a/framework/db/BatchQueryResult.php b/framework/db/BatchQueryResult.php index 774d416..3a1ba11 100644 --- a/framework/db/BatchQueryResult.php +++ b/framework/db/BatchQueryResult.php @@ -108,6 +108,7 @@ class BatchQueryResult extends Object implements \Iterator { if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) { $this->_batch = $this->fetchData(); + reset($this->_batch); } if ($this->each) { diff --git a/framework/db/Query.php b/framework/db/Query.php index 15d43d5..582e7f5 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -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]) diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index e6b6e01..124848b 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -599,7 +599,7 @@ class QueryBuilder extends \yii\base\Object if (strpos($column, '(') === false) { $column = $this->db->quoteColumnName($column); } - $columns[$i] = "$column AS " . $this->db->quoteColumnName($i);; + $columns[$i] = "$column AS " . $this->db->quoteColumnName($i); } elseif (strpos($column, '(') === false) { if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) { $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]); diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php index 1503a7e..4b3b1da 100644 --- a/framework/db/mssql/Schema.php +++ b/framework/db/mssql/Schema.php @@ -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; diff --git a/framework/db/oci/Schema.php b/framework/db/oci/Schema.php index e29a77b..1edbf6b 100644 --- a/framework/db/oci/Schema.php +++ b/framework/db/oci/Schema.php @@ -242,7 +242,7 @@ EOD; } elseif (strpos($dbType, 'NUMBER') !== false || strpos($dbType, 'INTEGER') !== false) { if (strpos($dbType, '(') && preg_match('/\((.*)\)/', $dbType, $matches)) { $values = explode(',', $matches[1]); - if (isset($values[1]) and (((int)$values[1]) > 0)) { + if (isset($values[1]) && (((int)$values[1]) > 0)) { $column->type = 'double'; } else { $column->type = 'integer'; diff --git a/framework/grid/ActionColumn.php b/framework/grid/ActionColumn.php index ffcfe0a..7d42bec 100644 --- a/framework/grid/ActionColumn.php +++ b/framework/grid/ActionColumn.php @@ -88,6 +88,7 @@ class ActionColumn extends Column $this->buttons['view'] = function ($url, $model) { return Html::a('<span class="glyphicon glyphicon-eye-open"></span>', $url, [ 'title' => Yii::t('yii', 'View'), + 'data-pjax' => '0', ]); }; } @@ -95,6 +96,7 @@ class ActionColumn extends Column $this->buttons['update'] = function ($url, $model) { return Html::a('<span class="glyphicon glyphicon-pencil"></span>', $url, [ 'title' => Yii::t('yii', 'Update'), + 'data-pjax' => '0', ]); }; } @@ -104,6 +106,7 @@ class ActionColumn extends Column 'title' => Yii::t('yii', 'Delete'), 'data-confirm' => Yii::t('yii', 'Are you sure to delete this item?'), 'data-method' => 'post', + 'data-pjax' => '0', ]); }; } diff --git a/framework/grid/Column.php b/framework/grid/Column.php index f09564d..77e0dca 100644 --- a/framework/grid/Column.php +++ b/framework/grid/Column.php @@ -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). diff --git a/framework/grid/DataColumn.php b/framework/grid/DataColumn.php index 559e3ab..7b0f493 100644 --- a/framework/grid/DataColumn.php +++ b/framework/grid/DataColumn.php @@ -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); } } diff --git a/framework/helpers/BaseArrayHelper.php b/framework/helpers/BaseArrayHelper.php index 0177494..278eaf0 100644 --- a/framework/helpers/BaseArrayHelper.php +++ b/framework/helpers/BaseArrayHelper.php @@ -58,35 +58,42 @@ class BaseArrayHelper */ public static function toArray($object, $properties = [], $recursive = true) { - if (!empty($properties) && is_object($object)) { - $className = get_class($object); - if (!empty($properties[$className])) { - $result = []; - foreach ($properties[$className] as $key => $name) { - if (is_int($key)) { - $result[$name] = $object->$name; - } else { - $result[$key] = static::getValue($object, $name); + if (is_array($object)) { + if ($recursive) { + foreach ($object as $key => $value) { + if (is_array($value) || is_object($value)) { + $object[$key] = static::toArray($value, true); } } - return $result; } - } - if ($object instanceof Arrayable) { - $object = $object->toArray(); - if (!$recursive) { - return $object; + return $object; + } elseif (is_object($object)) { + if (!empty($properties)) { + $className = get_class($object); + if (!empty($properties[$className])) { + $result = []; + foreach ($properties[$className] as $key => $name) { + if (is_int($key)) { + $result[$name] = $object->$name; + } else { + $result[$key] = static::getValue($object, $name); + } + } + return $recursive ? static::toArray($result) : $result; + } } - } - $result = []; - foreach ($object as $key => $value) { - if ($recursive && (is_array($value) || is_object($value))) { - $result[$key] = static::toArray($value, $properties, true); + if ($object instanceof Arrayable) { + $result = $object->toArray(); } else { - $result[$key] = $value; + $result = []; + foreach ($object as $key => $value) { + $result[$key] = $value; + } } + return $recursive ? static::toArray($result) : $result; + } else { + return [$object]; } - return $result; } /** diff --git a/framework/helpers/BaseFileHelper.php b/framework/helpers/BaseFileHelper.php index c544a91..6da4712 100644 --- a/framework/helpers/BaseFileHelper.php +++ b/framework/helpers/BaseFileHelper.php @@ -147,6 +147,7 @@ class BaseFileHelper * @param string $src the source directory * @param string $dst the destination directory * @param array $options options for directory copy. Valid options are: + * @throws \yii\base\InvalidParamException if unable to open directory * * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775. * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting. @@ -280,14 +281,14 @@ class BaseFileHelper $options['basePath'] = realpath($dir); // this should also be done only once if (isset($options['except'])) { - foreach($options['except'] as $key=>$value) { + foreach ($options['except'] as $key => $value) { if (is_string($value)) { $options['except'][$key] = static::parseExcludePattern($value); } } } if (isset($options['only'])) { - foreach($options['only'] as $key=>$value) { + foreach ($options['only'] as $key => $value) { if (is_string($value)) { $options['only'][$key] = static::parseExcludePattern($value); } @@ -397,7 +398,7 @@ class BaseFileHelper if ($pattern === $baseName) { return true; } - } else if ($flags & self::PATTERN_ENDSWITH) { + } elseif ($flags & self::PATTERN_ENDSWITH) { /* "*literal" matching against "fooliteral" */ $n = StringHelper::byteLength($pattern); if (StringHelper::byteSubstr($pattern, 1, $n) === StringHelper::byteSubstr($baseName, -$n, $n)) { @@ -472,7 +473,7 @@ class BaseFileHelper */ private static function lastExcludeMatchingFromList($basePath, $path, $excludes) { - foreach(array_reverse($excludes) as $exclude) { + foreach (array_reverse($excludes) as $exclude) { if (is_string($exclude)) { $exclude = self::parseExcludePattern($exclude); } @@ -508,13 +509,14 @@ class BaseFileHelper if (!is_string($pattern)) { throw new InvalidParamException('Exclude/include pattern must be a string.'); } - $result = array( + $result = [ 'pattern' => $pattern, 'flags' => 0, 'firstWildcard' => false, - ); - if (!isset($pattern[0])) + ]; + if (!isset($pattern[0])) { return $result; + } if ($pattern[0] == '!') { $result['flags'] |= self::PATTERN_NEGATIVE; @@ -526,11 +528,13 @@ class BaseFileHelper $len--; $result['flags'] |= self::PATTERN_MUSTBEDIR; } - if (strpos($pattern, '/') === false) + if (strpos($pattern, '/') === false) { $result['flags'] |= self::PATTERN_NODIR; + } $result['firstWildcard'] = self::firstWildcardInPattern($pattern); - if ($pattern[0] == '*' && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) + if ($pattern[0] == '*' && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) { $result['flags'] |= self::PATTERN_ENDSWITH; + } $result['pattern'] = $pattern; return $result; } @@ -542,8 +546,8 @@ class BaseFileHelper */ private static function firstWildcardInPattern($pattern) { - $wildcards = array('*','?','[','\\'); - $wildcardSearch = function($r, $c) use ($pattern) { + $wildcards = ['*', '?', '[', '\\']; + $wildcardSearch = function ($r, $c) use ($pattern) { $p = strpos($pattern, $c); return $r===false ? $p : ($p===false ? $r : min($r, $p)); }; diff --git a/framework/helpers/BaseHtml.php b/framework/helpers/BaseHtml.php index 01151ec..f0fe5b4 100644 --- a/framework/helpers/BaseHtml.php +++ b/framework/helpers/BaseHtml.php @@ -120,7 +120,7 @@ class BaseHtml * For example when using `['class' => 'my-class', 'target' => '_blank', 'value' => null]` it will result in the * html attributes rendered like this: `class="my-class" target="_blank"`. * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated HTML tag * @see beginTag() @@ -138,7 +138,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated start tag * @see endTag() * @see tag() @@ -167,7 +167,7 @@ class BaseHtml * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * If the options does not contain "type", a "type" attribute with value "text/css" will be used. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated style tag */ public static function style($content, $options = []) @@ -182,7 +182,7 @@ class BaseHtml * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated script tag */ public static function script($content, $options = []) @@ -196,7 +196,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated link tag * @see url() */ @@ -215,7 +215,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated script tag * @see url() */ @@ -235,7 +235,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated form start tag. * @see endForm() */ @@ -304,7 +304,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated hyperlink * @see url() */ @@ -326,7 +326,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated mailto link */ public static function mailto($text, $email = null, $options = []) @@ -341,7 +341,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated image tag */ public static function img($src, $options = []) @@ -363,7 +363,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated label tag */ public static function label($content, $for = null, $options = []) @@ -380,7 +380,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated button tag */ public static function button($content = 'Button', $options = []) @@ -396,7 +396,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated submit button tag */ public static function submitButton($content = 'Submit', $options = []) @@ -413,7 +413,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated reset button tag */ public static function resetButton($content = 'Reset', $options = []) @@ -430,7 +430,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated input tag */ public static function input($type, $name = null, $value = null, $options = []) @@ -447,7 +447,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated button tag */ public static function buttonInput($label = 'Button', $options = []) @@ -463,7 +463,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated button tag */ public static function submitInput($label = 'Submit', $options = []) @@ -478,7 +478,7 @@ class BaseHtml * @param string $label the value attribute. If it is null, the value attribute will not be generated. * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated button tag */ public static function resetInput($label = 'Reset', $options = []) @@ -495,7 +495,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated button tag */ public static function textInput($name, $value = null, $options = []) @@ -510,7 +510,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated button tag */ public static function hiddenInput($name, $value = null, $options = []) @@ -525,7 +525,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated button tag */ public static function passwordInput($name, $value = null, $options = []) @@ -543,7 +543,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated button tag */ public static function fileInput($name, $value = null, $options = []) @@ -558,7 +558,7 @@ class BaseHtml * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated text area tag */ public static function textarea($name, $value = '', $options = []) @@ -586,7 +586,7 @@ class BaseHtml * * The rest of the options will be rendered as the attributes of the resulting radio button tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated radio button tag */ @@ -636,7 +636,7 @@ class BaseHtml * * The rest of the options will be rendered as the attributes of the resulting checkbox tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated checkbox tag */ @@ -697,7 +697,7 @@ class BaseHtml * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated drop-down list tag */ @@ -744,7 +744,7 @@ class BaseHtml * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated list box tag */ @@ -800,7 +800,7 @@ class BaseHtml * is the label for the checkbox; and $name, $value and $checked represent the name, * value and the checked status of the checkbox input, respectively. * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated checkbox list */ @@ -871,7 +871,7 @@ class BaseHtml * is the label for the radio button; and $name, $value and $checked represent the name, * value and the checked status of the radio button input, respectively. * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated radio button list */ @@ -930,7 +930,7 @@ class BaseHtml * where $index is the array key corresponding to `$item` in `$items`. The callback should return * the whole list item tag. * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated unordered list. An empty string is returned if `$items` is empty. */ @@ -974,7 +974,7 @@ class BaseHtml * where $index is the array key corresponding to `$item` in `$items`. The callback should return * the whole list item tag. * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated ordered list. An empty string is returned if `$items` is empty. */ @@ -999,7 +999,7 @@ class BaseHtml * If this is not set, [[Model::getAttributeLabel()]] will be called to get the label for display * (after encoding). * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated label tag */ @@ -1025,7 +1025,7 @@ class BaseHtml * * - tag: this specifies the tag name. If not set, "div" will be used. * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated label tag */ @@ -1048,7 +1048,7 @@ class BaseHtml * about attribute expression. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated input tag */ public static function activeInput($type, $model, $attribute, $options = []) @@ -1070,7 +1070,7 @@ class BaseHtml * about attribute expression. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated input tag */ public static function activeTextInput($model, $attribute, $options = []) @@ -1087,7 +1087,7 @@ class BaseHtml * about attribute expression. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated input tag */ public static function activeHiddenInput($model, $attribute, $options = []) @@ -1104,7 +1104,7 @@ class BaseHtml * about attribute expression. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated input tag */ public static function activePasswordInput($model, $attribute, $options = []) @@ -1121,7 +1121,7 @@ class BaseHtml * about attribute expression. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated input tag */ public static function activeFileInput($model, $attribute, $options = []) @@ -1140,12 +1140,12 @@ class BaseHtml * about attribute expression. * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * @return string the generated textarea tag */ public static function activeTextarea($model, $attribute, $options = []) { - $name = static::getInputName($model, $attribute); + $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); $value = static::getAttributeValue($model, $attribute); if (!array_key_exists('id', $options)) { $options['id'] = static::getInputId($model, $attribute); @@ -1173,7 +1173,7 @@ class BaseHtml * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated radio button tag */ @@ -1216,7 +1216,7 @@ class BaseHtml * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated checkbox tag */ @@ -1272,7 +1272,7 @@ class BaseHtml * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated drop-down list tag */ @@ -1324,7 +1324,7 @@ class BaseHtml * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated list box tag */ @@ -1369,7 +1369,7 @@ class BaseHtml * is the label for the checkbox; and $name, $value and $checked represent the name, * value and the checked status of the checkbox input. * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated checkbox list */ @@ -1413,7 +1413,7 @@ class BaseHtml * is the label for the radio button; and $name, $value and $checked represent the name, * value and the checked status of the radio button input. * - * See [[renderTagAttributes()]] for details on how these are beeing rendered. + * See [[renderTagAttributes()]] for details on how these are being rendered. * * @return string the generated radio button list */ @@ -1579,7 +1579,7 @@ class BaseHtml { if (isset($options['class'])) { $classes = ' ' . $options['class'] . ' '; - if (($pos = strpos($classes, ' ' . $class . ' ')) === false) { + if (strpos($classes, ' ' . $class . ' ') === false) { $options['class'] .= ' ' . $class; } } else { diff --git a/framework/helpers/BaseInflector.php b/framework/helpers/BaseInflector.php index f9f19a8..3865588 100644 --- a/framework/helpers/BaseInflector.php +++ b/framework/helpers/BaseInflector.php @@ -275,7 +275,18 @@ class BaseInflector 'Ā' => 'A', 'Č' => 'C', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N', 'Š' => 'S', 'Ū' => 'u', 'Ž' => 'Z', 'ā' => 'a', 'č' => 'c', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n', - 'š' => 's', 'ū' => 'u', 'ž' => 'z' + 'š' => 's', 'ū' => 'u', 'ž' => 'z', + //Vietnamese + 'Ấ' => 'A', 'Ầ' => 'A', 'Ẩ' => 'A', 'Ẫ' => 'A', 'Ậ' => 'A', + 'Ắ' => 'A', 'Ằ' => 'A', 'Ẳ' => 'A', 'Ẵ' => 'A', 'Ặ' => 'A', + 'Ố' => 'O', 'Ồ' => 'O', 'Ổ' => 'O', 'Ỗ' => 'O', 'Ộ' => 'O', + 'Ớ' => 'O', 'Ờ' => 'O', 'Ở' => 'O', 'Ỡ' => 'O', 'Ợ' => 'O', + 'Ế' => 'E', 'Ề' => 'E', 'Ể' => 'E', 'Ễ' => 'E', 'Ệ' => 'E', + 'ấ' => 'a', 'ầ' => 'a', 'ẩ' => 'a', 'ẫ' => 'a', 'ậ' => 'a', + 'ắ' => 'a', 'ằ' => 'a', 'ẳ' => 'a', 'ẵ' => 'a', 'ặ' => 'a', + 'ố' => 'o', 'ồ' => 'o', 'ổ' => 'o', 'ỗ' => 'o', 'ộ' => 'o', + 'ớ' => 'o', 'ờ' => 'o', 'ở' => 'o', 'ỡ' => 'o', 'ợ' => 'o', + 'ế' => 'e', 'ề' => 'e', 'ể' => 'e', 'ễ' => 'e', 'ệ' => 'e' ]; /** diff --git a/framework/helpers/BaseMarkdown.php b/framework/helpers/BaseMarkdown.php index a22e45e..c72e079 100644 --- a/framework/helpers/BaseMarkdown.php +++ b/framework/helpers/BaseMarkdown.php @@ -86,7 +86,7 @@ class BaseMarkdown /** @var \cebe\markdown\Markdown $parser */ if (!isset(static::$flavors[$flavor])) { throw new InvalidParamException("Markdown flavor '$flavor' is not defined.'"); - } elseif(!is_object($config = static::$flavors[$flavor])) { + } elseif (!is_object($config = static::$flavors[$flavor])) { $parser = Yii::createObject($config); if (is_array($config)) { foreach ($config as $name => $value) { diff --git a/framework/helpers/BaseSecurity.php b/framework/helpers/BaseSecurity.php index d9459d9..8750a54 100644 --- a/framework/helpers/BaseSecurity.php +++ b/framework/helpers/BaseSecurity.php @@ -107,10 +107,10 @@ class BaseSecurity */ protected static function stripPadding($data) { - $end = StringHelper::byteSubstr($data, -1, NULL); + $end = StringHelper::byteSubstr($data, -1, null); $last = ord($end); $n = StringHelper::byteLength($data) - $last; - if (StringHelper::byteSubstr($data, $n, NULL) == str_repeat($end, $last)) { + if (StringHelper::byteSubstr($data, $n, null) == str_repeat($end, $last)) { return StringHelper::byteSubstr($data, 0, $n); } return false; diff --git a/framework/i18n/GettextMessageSource.php b/framework/i18n/GettextMessageSource.php index 8da8edc..fc6d87f 100644 --- a/framework/i18n/GettextMessageSource.php +++ b/framework/i18n/GettextMessageSource.php @@ -70,9 +70,9 @@ class GettextMessageSource extends MessageSource if ($messages === null && $fallbackMessages === null && $fallbackLanguage != $this->sourceLanguage) { Yii::error("The message file for category '$category' does not exist: $messageFile Fallback file does not exist as well: $fallbackMessageFile", __METHOD__); - } else if (empty($messages)) { + } elseif (empty($messages)) { return $fallbackMessages; - } else if (!empty($fallbackMessages)) { + } elseif (!empty($fallbackMessages)) { foreach ($fallbackMessages as $key => $value) { if (!empty($value) && empty($messages[$key])) { $messages[$key] = $fallbackMessages[$key]; diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index d0bbfcc..6be3899 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -126,7 +126,7 @@ class I18N extends Component } $p = []; - foreach($params as $name => $value) { + foreach ($params as $name => $value) { $p['{' . $name . '}'] = $value; } return strtr($message, $p); diff --git a/framework/i18n/MessageFormatter.php b/framework/i18n/MessageFormatter.php index 4ea7083..c3d8c56 100644 --- a/framework/i18n/MessageFormatter.php +++ b/framework/i18n/MessageFormatter.php @@ -143,7 +143,7 @@ class MessageFormatter extends Component return false; } $map = []; - foreach($tokens as $i => $token) { + foreach ($tokens as $i => $token) { if (is_array($token)) { $param = trim($token[0]); if (!isset($map[$param])) { @@ -169,7 +169,7 @@ class MessageFormatter extends Component return false; } else { $values = []; - foreach($result as $key => $value) { + foreach ($result as $key => $value) { $values[$map[$key]] = $value; } return $values; @@ -190,7 +190,7 @@ class MessageFormatter extends Component if (($tokens = self::tokenizePattern($pattern)) === false) { return false; } - foreach($tokens as $i => $token) { + foreach ($tokens as $i => $token) { if (!is_array($token)) { continue; } @@ -210,7 +210,7 @@ class MessageFormatter extends Component } $type = isset($token[1]) ? trim($token[1]) : 'none'; // replace plural and select format recursively - if ($type == 'plural' || $type == 'select') { + if ($type == 'plural' || $type == 'select') { if (!isset($token[2])) { return false; } @@ -244,7 +244,7 @@ class MessageFormatter extends Component $this->_errorMessage = "Message pattern is invalid."; return false; } - foreach($tokens as $i => $token) { + foreach ($tokens as $i => $token) { if (is_array($token)) { if (($tokens[$i] = $this->parseToken($token, $args, $locale)) === false) { $this->_errorCode = -1; diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 2e611f5..7866511 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -73,9 +73,9 @@ class PhpMessageSource extends MessageSource if ($messages === null && $fallbackMessages === null && $fallbackLanguage != $this->sourceLanguage) { Yii::error("The message file for category '$category' does not exist: $messageFile Fallback file does not exist as well: $fallbackMessageFile", __METHOD__); - } else if (empty($messages)) { + } elseif (empty($messages)) { return $fallbackMessages; - } else if (!empty($fallbackMessages)) { + } elseif (!empty($fallbackMessages)) { foreach ($fallbackMessages as $key => $value) { if (!empty($value) && empty($messages[$key])) { $messages[$key] = $fallbackMessages[$key]; diff --git a/framework/mail/BaseMailer.php b/framework/mail/BaseMailer.php index 139b4af..01f6635 100644 --- a/framework/mail/BaseMailer.php +++ b/framework/mail/BaseMailer.php @@ -348,5 +348,4 @@ abstract class BaseMailer extends Component implements MailerInterface, ViewCont $event = new MailEvent(['message' => $message, 'isSuccessful' => $isSuccessful]); $this->trigger(self::EVENT_AFTER_SEND, $event); } - } diff --git a/framework/messages/pt-PT/yii.php b/framework/messages/pt-PT/yii.php index 375b4d6..2d383c0 100644 --- a/framework/messages/pt-PT/yii.php +++ b/framework/messages/pt-PT/yii.php @@ -1,4 +1,4 @@ -<?php +<?php /** * Message translations. * diff --git a/framework/requirements/views/web/index.php b/framework/requirements/views/web/index.php index 287d4bb..a2aa59d 100644 --- a/framework/requirements/views/web/index.php +++ b/framework/requirements/views/web/index.php @@ -27,7 +27,7 @@ </p> <p> There are two kinds of requirements being checked. Mandatory requirements are those that have to be met - to allow Yii to work as expected. There are also some optional requirements beeing checked which will + to allow Yii to work as expected. There are also some optional requirements being checked which will show you a warning when they do not meet. You can use Yii framework without them but some specific functionality may be not available in this case. </p> diff --git a/framework/rest/Action.php b/framework/rest/Action.php new file mode 100644 index 0000000..1a98539 --- /dev/null +++ b/framework/rest/Action.php @@ -0,0 +1,106 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\base\InvalidConfigException; +use yii\db\ActiveRecordInterface; +use yii\web\NotFoundHttpException; + +/** + * Action is the base class for action classes that implement RESTful API. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Action extends \yii\base\Action +{ + /** + * @var string class name of the model which will be handled by this action. + * The model class must implement [[ActiveRecordInterface]]. + * This property must be set. + */ + public $modelClass; + /** + * @var callable a PHP callable that will be called to return the model corresponding + * to the specified primary key value. If not set, [[findModel()]] will be used instead. + * The signature of the callable should be: + * + * ```php + * function ($id, $action) { + * // $id is the primary key value. If composite primary key, the key values + * // will be separated by comma. + * // $action is the action object currently running + * } + * ``` + * + * The callable should return the model found, or throw an exception if not found. + */ + public $findModel; + /** + * @var callable a PHP callable that will be called when running an action to determine + * if the current user has the permission to execute the action. If not set, the access + * check will not be performed. The signature of the callable should be as follows, + * + * ```php + * function ($action, $model = null) { + * // $model is the requested model instance. + * // If null, it means no specific model (e.g. IndexAction) + * } + * ``` + */ + public $checkAccess; + + + /** + * @inheritdoc + */ + public function init() + { + if ($this->modelClass === null) { + throw new InvalidConfigException(get_class($this) . '::$modelClass must be set.'); + } + } + + /** + * Returns the data model based on the primary key given. + * If the data model is not found, a 404 HTTP exception will be raised. + * @param string $id the ID of the model to be loaded. If the model has a composite primary key, + * the ID must be a string of the primary key values separated by commas. + * The order of the primary key values should follow that returned by the `primaryKey()` method + * of the model. + * @return ActiveRecordInterface the model found + * @throws NotFoundHttpException if the model cannot be found + */ + public function findModel($id) + { + if ($this->findModel !== null) { + return call_user_func($this->findModel, $id, $this); + } + + /** + * @var ActiveRecordInterface $modelClass + */ + $modelClass = $this->modelClass; + $keys = $modelClass::primaryKey(); + if (count($keys) > 1) { + $values = explode(',', $id); + if (count($keys) === count($values)) { + $model = $modelClass::find(array_combine($keys, $values)); + } + } elseif ($id !== null) { + $model = $modelClass::find($id); + } + + if (isset($model)) { + return $model; + } else { + throw new NotFoundHttpException("Object not found: $id"); + } + } +} diff --git a/framework/rest/ActiveController.php b/framework/rest/ActiveController.php new file mode 100644 index 0000000..75a4f55 --- /dev/null +++ b/framework/rest/ActiveController.php @@ -0,0 +1,126 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use yii\base\InvalidConfigException; +use yii\base\Model; + +/** + * ActiveController implements a common set of actions for supporting RESTful access to ActiveRecord. + * + * The class of the ActiveRecord should be specified via [[modelClass]], which must implement [[\yii\db\ActiveRecordInterface]]. + * By default, the following actions are supported: + * + * - `index`: list of models + * - `view`: return the details of a model + * - `create`: create a new model + * - `update`: update an existing model + * - `delete`: delete an existing model + * - `options`: return the allowed HTTP methods + * + * You may disable some of these actions by overriding [[actions()]] and unsetting the corresponding actions. + * + * To add a new action, either override [[actions()]] by appending a new action class or write a new action method. + * Make sure you also override [[verbs()]] to properly declare what HTTP methods are allowed by the new action. + * + * You should usually override [[checkAccess()]] to check whether the current user has the privilege to perform + * the specified action against the specified model. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ActiveController extends Controller +{ + /** + * @var string the model class name. This property must be set. + */ + public $modelClass; + /** + * @var string the scenario used for updating a model. + * @see \yii\base\Model::scenarios() + */ + public $updateScenario = Model::SCENARIO_DEFAULT; + /** + * @var string the scenario used for creating a model. + * @see \yii\base\Model::scenarios() + */ + public $createScenario = Model::SCENARIO_DEFAULT; + /** + * @var boolean whether to use a DB transaction when creating, updating or deleting a model. + * This property is only useful for relational database. + */ + public $transactional = true; + + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->modelClass === null) { + throw new InvalidConfigException('The "modelClass" property must be set.'); + } + } + + /** + * @inheritdoc + */ + public function actions() + { + return [ + 'index' => [ + 'class' => 'yii\rest\IndexAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + ], + 'view' => [ + 'class' => 'yii\rest\ViewAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + ], + 'create' => [ + 'class' => 'yii\rest\CreateAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + 'scenario' => $this->createScenario, + 'transactional' => $this->transactional, + ], + 'update' => [ + 'class' => 'yii\rest\UpdateAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + 'scenario' => $this->updateScenario, + 'transactional' => $this->transactional, + ], + 'delete' => [ + 'class' => 'yii\rest\DeleteAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + 'transactional' => $this->transactional, + ], + 'options' => [ + 'class' => 'yii\rest\OptionsAction', + ], + ]; + } + + /** + * @inheritdoc + */ + protected function verbs() + { + return [ + 'index' => ['GET', 'HEAD'], + 'view' => ['GET', 'HEAD'], + 'create' => ['POST'], + 'update' => ['PUT', 'PATCH'], + 'delete' => ['DELETE'], + ]; + } +} diff --git a/framework/rest/AuthInterface.php b/framework/rest/AuthInterface.php new file mode 100644 index 0000000..30ccc9f --- /dev/null +++ b/framework/rest/AuthInterface.php @@ -0,0 +1,41 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use yii\web\User; +use yii\web\Request; +use yii\web\Response; +use yii\web\IdentityInterface; +use yii\web\UnauthorizedHttpException; + +/** + * AuthInterface is the interface required by classes that support user authentication. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +interface AuthInterface +{ + /** + * Authenticates the current user. + * + * @param User $user + * @param Request $request + * @param Response $response + * @return IdentityInterface the authenticated user identity. If authentication information is not provided, null will be returned. + * @throws UnauthorizedHttpException if authentication information is provided but is invalid. + */ + public function authenticate($user, $request, $response); + /** + * Handles authentication failure. + * The implementation should normally throw UnauthorizedHttpException to indicate authentication failure. + * @param Response $response + * @throws UnauthorizedHttpException + */ + public function handleFailure($response); +} diff --git a/framework/rest/Controller.php b/framework/rest/Controller.php new file mode 100644 index 0000000..7900b15 --- /dev/null +++ b/framework/rest/Controller.php @@ -0,0 +1,247 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\base\InvalidConfigException; +use yii\web\Response; +use yii\web\UnauthorizedHttpException; +use yii\web\UnsupportedMediaTypeHttpException; +use yii\web\TooManyRequestsHttpException; +use yii\web\VerbFilter; +use yii\web\ForbiddenHttpException; + +/** + * Controller is the base class for RESTful API controller classes. + * + * Controller implements the following steps in a RESTful API request handling cycle: + * + * 1. Resolving response format and API version number (see [[supportedFormats]], [[supportedVersions]] and [[version]]); + * 2. Validating request method (see [[verbs()]]). + * 3. Authenticating user (see [[authenticate()]]); + * 4. Formatting response data (see [[serializeData()]]). + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Controller extends \yii\web\Controller +{ + /** + * @var string the name of the header parameter representing the API version number. + */ + public $versionHeaderParam = 'version'; + /** + * @var string|array the configuration for creating the serializer that formats the response data. + */ + public $serializer = 'yii\rest\Serializer'; + /** + * @inheritdoc + */ + public $enableCsrfValidation = false; + /** + * @var array the supported authentication methods. This property should take a list of supported + * authentication methods, each represented by an authentication class or configuration. + * If this is not set or empty, it means authentication is disabled. + */ + public $authMethods; + /** + * @var string|array the rate limiter class or configuration. If this is not set or empty, + * the rate limiting will be disabled. Note that if the user is not authenticated, the rate limiting + * will also NOT be performed. + * @see checkRateLimit() + * @see authMethods + */ + public $rateLimiter = 'yii\rest\RateLimiter'; + /** + * @var string the chosen API version number, or null if [[supportedVersions]] is empty. + * @see supportedVersions + */ + public $version; + /** + * @var array list of supported API version numbers. If the current request does not specify a version + * number, the first element will be used as the [[version|chosen version number]]. For this reason, you should + * put the latest version number at the first. If this property is empty, [[version]] will not be set. + */ + public $supportedVersions = []; + /** + * @var array list of supported response formats. The array keys are the requested content MIME types, + * and the array values are the corresponding response formats. The first element will be used + * as the response format if the current request does not specify a content type. + */ + public $supportedFormats = [ + 'application/json' => Response::FORMAT_JSON, + 'application/xml' => Response::FORMAT_XML, + ]; + + /** + * @inheritdoc + */ + public function behaviors() + { + return [ + 'verbFilter' => [ + 'class' => VerbFilter::className(), + 'actions' => $this->verbs(), + ], + ]; + } + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + $this->resolveFormatAndVersion(); + } + + /** + * @inheritdoc + */ + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + $this->authenticate(); + $this->checkRateLimit($action); + return true; + } else { + return false; + } + } + + /** + * @inheritdoc + */ + public function afterAction($action, $result) + { + $result = parent::afterAction($action, $result); + return $this->serializeData($result); + } + + /** + * Resolves the response format and the API version number. + * @throws UnsupportedMediaTypeHttpException + */ + protected function resolveFormatAndVersion() + { + $this->version = empty($this->supportedVersions) ? null : reset($this->supportedVersions); + Yii::$app->getResponse()->format = reset($this->supportedFormats); + $types = Yii::$app->getRequest()->getAcceptableContentTypes(); + if (empty($types)) { + $types['*/*'] = []; + } + + foreach ($types as $type => $params) { + if (isset($this->supportedFormats[$type])) { + Yii::$app->getResponse()->format = $this->supportedFormats[$type]; + if (isset($params[$this->versionHeaderParam])) { + if (in_array($params[$this->versionHeaderParam], $this->supportedVersions, true)) { + $this->version = $params[$this->versionHeaderParam]; + } else { + throw new UnsupportedMediaTypeHttpException('You are requesting an invalid version number.'); + } + } + return; + } + } + + if (!isset($types['*/*'])) { + throw new UnsupportedMediaTypeHttpException('None of your requested content types is supported.'); + } + } + + /** + * Declares the allowed HTTP verbs. + * Please refer to [[VerbFilter::actions]] on how to declare the allowed verbs. + * @return array the allowed HTTP verbs. + */ + protected function verbs() + { + return []; + } + + /** + * Authenticates the user. + * This method implements the user authentication based on an access token sent through the `Authorization` HTTP header. + * @throws UnauthorizedHttpException if the user is not authenticated successfully + */ + protected function authenticate() + { + if (empty($this->authMethods)) { + return; + } + + $user = Yii::$app->getUser(); + $request = Yii::$app->getRequest(); + $response = Yii::$app->getResponse(); + foreach ($this->authMethods as $i => $auth) { + $this->authMethods[$i] = $auth = Yii::createObject($auth); + if (!$auth instanceof AuthInterface) { + throw new InvalidConfigException(get_class($auth) . ' must implement yii\rest\AuthInterface'); + } elseif ($auth->authenticate($user, $request, $response) !== null) { + return; + } + } + + /** @var AuthInterface $auth */ + $auth = reset($this->authMethods); + $auth->handleFailure($response); + } + + /** + * Ensures the rate limit is not exceeded. + * + * This method will use [[rateLimiter]] to check rate limit. In order to perform rate limiting check, + * the user must be authenticated and the user identity object (`Yii::$app->user->identity`) must + * implement [[RateLimitInterface]]. + * + * @param \yii\base\Action $action the action to be executed + * @throws TooManyRequestsHttpException if the rate limit is exceeded. + */ + protected function checkRateLimit($action) + { + if (empty($this->rateLimiter)) { + return; + } + + $identity = Yii::$app->getUser()->getIdentity(false); + if ($identity instanceof RateLimitInterface) { + /** @var RateLimiter $rateLimiter */ + $rateLimiter = Yii::createObject($this->rateLimiter); + $rateLimiter->check($identity, Yii::$app->getRequest(), Yii::$app->getResponse(), $action); + } + } + + /** + * Serializes the specified data. + * The default implementation will create a serializer based on the configuration given by [[serializer]]. + * It then uses the serializer to serialize the given data. + * @param mixed $data the data to be serialized + * @return mixed the serialized data. + */ + protected function serializeData($data) + { + return Yii::createObject($this->serializer)->serialize($data); + } + + /** + * Checks the privilege of the current user. + * + * This method should be overridden to check whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param object $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws ForbiddenHttpException if the user does not have access + */ + public function checkAccess($action, $model = null, $params = []) + { + } +} diff --git a/framework/rest/CreateAction.php b/framework/rest/CreateAction.php new file mode 100644 index 0000000..fa818c2 --- /dev/null +++ b/framework/rest/CreateAction.php @@ -0,0 +1,80 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\base\Model; +use yii\db\ActiveRecord; + +/** + * CreateAction implements the API endpoint for creating a new model from the given data. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class CreateAction extends Action +{ + /** + * @var string the scenario to be assigned to the new model before it is validated and saved. + */ + public $scenario = Model::SCENARIO_DEFAULT; + /** + * @var boolean whether to start a DB transaction when saving the model. + */ + public $transactional = true; + /** + * @var string the name of the view action. This property is need to create the URL when the mode is successfully created. + */ + public $viewAction = 'view'; + + + /** + * Creates a new model. + * @return \yii\db\ActiveRecordInterface the model newly created + * @throws \Exception if there is any error when creating the model + */ + public function run() + { + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id); + } + + /** + * @var \yii\db\ActiveRecord $model + */ + $model = new $this->modelClass([ + 'scenario' => $this->scenario, + ]); + + $model->load(Yii::$app->getRequest()->getBodyParams(), ''); + + if ($this->transactional && $model instanceof ActiveRecord) { + if ($model->validate()) { + $transaction = $model->getDb()->beginTransaction(); + try { + $model->insert(false); + $transaction->commit(); + } catch (\Exception $e) { + $transaction->rollback(); + throw $e; + } + } + } else { + $model->save(); + } + + if (!$model->hasErrors()) { + $response = Yii::$app->getResponse(); + $response->setStatusCode(201); + $id = implode(',', array_values($model->getPrimaryKey(true))); + $response->getHeaders()->set('Location', $this->controller->createAbsoluteUrl([$this->viewAction, 'id' => $id])); + } + + return $model; + } +} diff --git a/framework/rest/DeleteAction.php b/framework/rest/DeleteAction.php new file mode 100644 index 0000000..a0355c8 --- /dev/null +++ b/framework/rest/DeleteAction.php @@ -0,0 +1,53 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\db\ActiveRecord; + +/** + * DeleteAction implements the API endpoint for deleting a model. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class DeleteAction extends Action +{ + /** + * @var boolean whether to start a DB transaction when deleting the model. + */ + public $transactional = true; + + + /** + * Deletes a model. + */ + public function run($id) + { + $model = $this->findModel($id); + + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id, $model); + } + + if ($this->transactional && $model instanceof ActiveRecord) { + $transaction = $model->getDb()->beginTransaction(); + try { + $model->delete(); + $transaction->commit(); + } catch (\Exception $e) { + $transaction->rollback(); + throw $e; + } + } else { + $model->delete(); + } + + Yii::$app->getResponse()->setStatusCode(204); + } +} diff --git a/framework/rest/HttpBasicAuth.php b/framework/rest/HttpBasicAuth.php new file mode 100644 index 0000000..7a69c15 --- /dev/null +++ b/framework/rest/HttpBasicAuth.php @@ -0,0 +1,50 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\base\Component; +use yii\web\UnauthorizedHttpException; + +/** + * HttpBasicAuth implements the HTTP Basic authentication method. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class HttpBasicAuth extends Component implements AuthInterface +{ + /** + * @var string the HTTP authentication realm + */ + public $realm = 'api'; + + /** + * @inheritdoc + */ + public function authenticate($user, $request, $response) + { + if (($accessToken = $request->getAuthUser()) !== null) { + $identity = $user->loginByAccessToken($accessToken); + if ($identity !== null) { + return $identity; + } + $this->handleFailure($response); + } + return null; + } + + /** + * @inheritdoc + */ + public function handleFailure($response) + { + $response->getHeaders()->set('WWW-Authenticate', "Basic realm=\"{$this->realm}\""); + throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); + } +} diff --git a/framework/rest/HttpBearerAuth.php b/framework/rest/HttpBearerAuth.php new file mode 100644 index 0000000..81033c9 --- /dev/null +++ b/framework/rest/HttpBearerAuth.php @@ -0,0 +1,52 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\base\Component; +use yii\web\UnauthorizedHttpException; + +/** + * HttpBearerAuth implements the authentication method based on HTTP Bearer token. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class HttpBearerAuth extends Component implements AuthInterface +{ + /** + * @var string the HTTP authentication realm + */ + public $realm = 'api'; + + /** + * @inheritdoc + */ + public function authenticate($user, $request, $response) + { + $authHeader = $request->getHeaders()->get('Authorization'); + if ($authHeader !== null && preg_match("/^Bearer\\s+(.*?)$/", $authHeader, $matches)) { + $identity = $user->loginByAccessToken($matches[1]); + if ($identity !== null) { + return $identity; + } + + $this->handleFailure($response); + } + return null; + } + + /** + * @inheritdoc + */ + public function handleFailure($response) + { + $response->getHeaders()->set('WWW-Authenticate', "Basic realm=\"{$this->realm}\""); + throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); + } +} diff --git a/framework/rest/IndexAction.php b/framework/rest/IndexAction.php new file mode 100644 index 0000000..ca30220 --- /dev/null +++ b/framework/rest/IndexAction.php @@ -0,0 +1,65 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\data\ActiveDataProvider; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class IndexAction extends Action +{ + /** + * @var callable a PHP callable that will be called to prepare a data provider that + * should return a collection of the models. If not set, [[prepareDataProvider()]] will be used instead. + * The signature of the callable should be: + * + * ```php + * function ($action) { + * // $action is the action object currently running + * } + * ``` + * + * The callable should return an instance of [[ActiveDataProvider]]. + */ + public $prepareDataProvider; + + + /** + * @return ActiveDataProvider + */ + public function run() + { + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id); + } + + return $this->prepareDataProvider(); + } + + /** + * Prepares the data provider that should return the requested collection of the models. + * @return ActiveDataProvider + */ + protected function prepareDataProvider() + { + if ($this->prepareDataProvider !== null) { + return call_user_func($this->prepareDataProvider, $this); + } + + /** + * @var \yii\db\BaseActiveRecord $modelClass + */ + $modelClass = $this->modelClass; + return new ActiveDataProvider([ + 'query' => $modelClass::find(), + ]); + } +} diff --git a/framework/rest/OptionsAction.php b/framework/rest/OptionsAction.php new file mode 100644 index 0000000..0f9561f --- /dev/null +++ b/framework/rest/OptionsAction.php @@ -0,0 +1,42 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; + +/** + * OptionsAction responds to the OPTIONS request by sending back an `Allow` header. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class OptionsAction extends \yii\base\Action +{ + /** + * @var array the HTTP verbs that are supported by the collection URL + */ + public $collectionOptions = ['GET', 'POST', 'HEAD', 'OPTIONS']; + /** + * @var array the HTTP verbs that are supported by the resource URL + */ + public $resourceOptions = ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; + + + /** + * Responds to the OPTIONS request. + * @param string $id + */ + public function run($id = null) + { + if (Yii::$app->getRequest()->getMethod() !== 'OPTIONS') { + Yii::$app->getResponse()->setStatusCode(405); + } + $options = $id === null ? $this->collectionOptions : $this->resourceOptions; + Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $options)); + } +} diff --git a/framework/rest/QueryParamAuth.php b/framework/rest/QueryParamAuth.php new file mode 100644 index 0000000..f45e4c8 --- /dev/null +++ b/framework/rest/QueryParamAuth.php @@ -0,0 +1,52 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\base\Component; +use yii\web\UnauthorizedHttpException; + +/** + * QueryParamAuth implements the authentication method based on the access token passed through a query parameter. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class QueryParamAuth extends Component implements AuthInterface +{ + /** + * @var string the parameter name for passing the access token + */ + public $tokenParam = 'access-token'; + + /** + * @inheritdoc + */ + public function authenticate($user, $request, $response) + { + $accessToken = $request->get($this->tokenParam); + if (is_string($accessToken)) { + $identity = $user->loginByAccessToken($accessToken); + if ($identity !== null) { + return $identity; + } + } + if ($accessToken !== null) { + $this->handleFailure($response); + } + return null; + } + + /** + * @inheritdoc + */ + public function handleFailure($response) + { + throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); + } +} diff --git a/framework/rest/RateLimitInterface.php b/framework/rest/RateLimitInterface.php new file mode 100644 index 0000000..07f60e0 --- /dev/null +++ b/framework/rest/RateLimitInterface.php @@ -0,0 +1,39 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +/** + * RateLimitInterface is the interface that may be implemented by an identity object to enforce rate limiting. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +interface RateLimitInterface +{ + /** + * Returns the maximum number of allowed requests and the window size. + * @param array $params the additional parameters associated with the rate limit. + * @return array an array of two elements. The first element is the maximum number of allowed requests, + * and the second element is the size of the window in seconds. + */ + public function getRateLimit($params = []); + /** + * Loads the number of allowed requests and the corresponding timestamp from a persistent storage. + * @param array $params the additional parameters associated with the rate limit. + * @return array an array of two elements. The first element is the number of allowed requests, + * and the second element is the corresponding UNIX timestamp. + */ + public function loadAllowance($params = []); + /** + * Saves the number of allowed requests and the corresponding timestamp to a persistent storage. + * @param integer $allowance the number of allowed requests remaining. + * @param integer $timestamp the current timestamp. + * @param array $params the additional parameters associated with the rate limit. + */ + public function saveAllowance($allowance, $timestamp, $params = []); +} diff --git a/framework/rest/RateLimiter.php b/framework/rest/RateLimiter.php new file mode 100644 index 0000000..753a0f0 --- /dev/null +++ b/framework/rest/RateLimiter.php @@ -0,0 +1,85 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use yii\base\Component; +use yii\base\Action; +use yii\web\Request; +use yii\web\Response; +use yii\web\TooManyRequestsHttpException; + +/** + * RateLimiter implements a rate limiting algorithm based on the [leaky bucket algorithm](http://en.wikipedia.org/wiki/Leaky_bucket). + * + * You may call [[check()]] to enforce rate limiting. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class RateLimiter extends Component +{ + /** + * @var boolean whether to include rate limit headers in the response + */ + public $enableRateLimitHeaders = true; + /** + * @var string the message to be displayed when rate limit exceeds + */ + public $errorMessage = 'Rate limit exceeded.'; + + /** + * Checks whether the rate limit exceeds. + * @param RateLimitInterface $user the current user + * @param Request $request + * @param Response $response + * @param Action $action the action to be executed + * @throws TooManyRequestsHttpException if rate limit exceeds + */ + public function check($user, $request, $response, $action) + { + $current = time(); + $params = [ + 'request' => $request, + 'action' => $action, + ]; + + list ($limit, $window) = $user->getRateLimit($params); + list ($allowance, $timestamp) = $user->loadAllowance($params); + + $allowance += (int)(($current - $timestamp) * $limit / $window); + if ($allowance > $limit) { + $allowance = $limit; + } + + if ($allowance < 1) { + $user->saveAllowance(0, $current, $params); + $this->addRateLimitHeaders($response, $limit, 0, $window); + throw new TooManyRequestsHttpException($this->errorMessage); + } else { + $user->saveAllowance($allowance - 1, $current, $params); + $this->addRateLimitHeaders($response, $limit, 0, (int)(($limit - $allowance) * $window / $limit)); + } + } + + /** + * Adds the rate limit headers to the response + * @param Response $response + * @param integer $limit the maximum number of allowed requests during a period + * @param integer $remaining the remaining number of allowed requests within the current period + * @param integer $reset the number of seconds to wait before having maximum number of allowed requests again + */ + protected function addRateLimitHeaders($response, $limit, $remaining, $reset) + { + if ($this->enableRateLimitHeaders) { + $response->getHeaders() + ->set('X-Rate-Limit-Limit', $limit) + ->set('X-Rate-Limit-Remaining', $remaining) + ->set('X-Rate-Limit-Reset', $reset); + } + } +} diff --git a/framework/rest/Serializer.php b/framework/rest/Serializer.php new file mode 100644 index 0000000..75a6664 --- /dev/null +++ b/framework/rest/Serializer.php @@ -0,0 +1,248 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\base\Component; +use yii\base\Model; +use yii\data\DataProviderInterface; +use yii\data\Pagination; +use yii\helpers\ArrayHelper; +use yii\web\Link; +use yii\web\Request; +use yii\web\Response; + +/** + * Serializer converts resource objects and collections into array representation. + * + * Serializer is mainly used by REST controllers to convert different objects into array representation + * so that they can be further turned into different formats, such as JSON, XML, by response formatters. + * + * The default implementation handles resources as [[Model]] objects and collections as objects + * implementing [[DataProviderInterface]]. You may override [[serialize()]] to handle more types. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Serializer extends Component +{ + /** + * @var string the name of the query parameter containing the information about which fields should be returned + * for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined + * by [[Model::fields()]] will be returned. + */ + public $fieldsParam = 'fields'; + /** + * @var string the name of the query parameter containing the information about which fields should be returned + * in addition to those listed in [[fieldsParam]] for a resource object. + */ + public $expandParam = 'expand'; + /** + * @var string the name of the HTTP header containing the information about total number of data items. + * This is used when serving a resource collection with pagination. + */ + public $totalCountHeader = 'X-Pagination-Total-Count'; + /** + * @var string the name of the HTTP header containing the information about total number of pages of data. + * This is used when serving a resource collection with pagination. + */ + public $pageCountHeader = 'X-Pagination-Page-Count'; + /** + * @var string the name of the HTTP header containing the information about the current page number (1-based). + * This is used when serving a resource collection with pagination. + */ + public $currentPageHeader = 'X-Pagination-Current-Page'; + /** + * @var string the name of the HTTP header containing the information about the number of data items in each page. + * This is used when serving a resource collection with pagination. + */ + public $perPageHeader = 'X-Pagination-Per-Page'; + /** + * @var string the name of the envelope (e.g. `items`) for returning the resource objects in a collection. + * This is used when serving a resource collection. When this is set and pagination is enabled, the serializer + * will return a collection in the following format: + * + * ```php + * [ + * 'items' => [...], // assuming collectionEnvelope is "items" + * '_links' => { // pagination links as returned by Pagination::getLinks() + * 'self' => '...', + * 'next' => '...', + * 'last' => '...', + * }, + * '_meta' => { // meta information as returned by Pagination::toArray() + * 'totalCount' => 100, + * 'pageCount' => 5, + * 'currentPage' => 1, + * 'perPage' => 20, + * }, + * ] + * ``` + * + * If this property is not set, the resource arrays will be directly returned without using envelope. + * The pagination information as shown in `_links` and `_meta` can be accessed from the response HTTP headers. + */ + public $collectionEnvelope; + /** + * @var Request the current request. If not set, the `request` application component will be used. + */ + public $request; + /** + * @var Response the response to be sent. If not set, the `response` application component will be used. + */ + public $response; + + /** + * @inheritdoc + */ + public function init() + { + if ($this->request === null) { + $this->request = Yii::$app->getRequest(); + } + if ($this->response === null) { + $this->response = Yii::$app->getResponse(); + } + } + + /** + * Serializes the given data into a format that can be easily turned into other formats. + * This method mainly converts the objects of recognized types into array representation. + * It will not do conversion for unknown object types or non-object data. + * The default implementation will handle [[Model]] and [[DataProviderInterface]]. + * You may override this method to support more object types. + * @param mixed $data the data to be serialized. + * @return mixed the converted data. + */ + public function serialize($data) + { + if ($data instanceof Model) { + return $data->hasErrors() ? $this->serializeModelErrors($data) : $this->serializeModel($data); + } elseif ($data instanceof DataProviderInterface) { + return $this->serializeDataProvider($data); + } else { + return $data; + } + } + + /** + * @return array the names of the requested fields. The first element is an array + * representing the list of default fields requested, while the second element is + * an array of the extra fields requested in addition to the default fields. + * @see Model::fields() + * @see Model::extraFields() + */ + protected function getRequestedFields() + { + $fields = $this->request->get($this->fieldsParam); + $expand = $this->request->get($this->expandParam); + return [ + preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY), + preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY), + ]; + } + + /** + * Serializes a data provider. + * @param DataProviderInterface $dataProvider + * @return array the array representation of the data provider. + */ + protected function serializeDataProvider($dataProvider) + { + $models = $this->serializeModels($dataProvider->getModels()); + + if (($pagination = $dataProvider->getPagination()) !== false) { + $this->addPaginationHeaders($pagination); + } + + if ($this->request->getIsHead()) { + return null; + } elseif ($this->collectionEnvelope === null) { + return $models; + } else { + $result = [ + $this->collectionEnvelope => $models, + ]; + if ($pagination !== false) { + $result['_links'] = Link::serialize($pagination->getLinks()); + $result['_meta'] = $pagination->toArray(); + } + return $result; + } + } + + /** + * Adds HTTP headers about the pagination to the response. + * @param Pagination $pagination + */ + protected function addPaginationHeaders($pagination) + { + $links = []; + foreach ($pagination->getLinks(true) as $rel => $url) { + $links[] = "<$url>; rel=$rel"; + } + + $this->response->getHeaders() + ->set($this->totalCountHeader, $pagination->totalCount) + ->set($this->pageCountHeader, $pagination->getPageCount()) + ->set($this->currentPageHeader, $pagination->getPage() + 1) + ->set($this->perPageHeader, $pagination->pageSize) + ->set('Link', implode(', ', $links)); + } + + /** + * Serializes a model object. + * @param Model $model + * @return array the array representation of the model + */ + protected function serializeModel($model) + { + if ($this->request->getIsHead()) { + return null; + } else { + list ($fields, $expand) = $this->getRequestedFields(); + return $model->toArray($fields, $expand); + } + } + + /** + * Serializes the validation errors in a model. + * @param Model $model + * @return array the array representation of the errors + */ + protected function serializeModelErrors($model) + { + $this->response->setStatusCode(422, 'Data Validation Failed.'); + $result = []; + foreach ($model->getFirstErrors() as $name => $message) { + $result[] = [ + 'field' => $name, + 'message' => $message, + ]; + } + return $result; + } + + /** + * Serializes a set of models. + * @param array $models + * @return array the array representation of the models + */ + protected function serializeModels(array $models) + { + list ($fields, $expand) = $this->getRequestedFields(); + foreach ($models as $i => $model) { + if ($model instanceof Model) { + $models[$i] = $model->toArray($fields, $expand); + } elseif (is_array($model)) { + $models[$i] = ArrayHelper::toArray($model); + } + } + return $models; + } +} diff --git a/framework/rest/UpdateAction.php b/framework/rest/UpdateAction.php new file mode 100644 index 0000000..7a14a0a --- /dev/null +++ b/framework/rest/UpdateAction.php @@ -0,0 +1,67 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\base\Model; +use yii\db\ActiveRecord; + +/** + * UpdateAction implements the API endpoint for updating a model. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class UpdateAction extends Action +{ + /** + * @var string the scenario to be assigned to the model before it is validated and updated. + */ + public $scenario = Model::SCENARIO_DEFAULT; + /** + * @var boolean whether to start a DB transaction when saving the model. + */ + public $transactional = true; + + + /** + * Updates an existing model. + * @param string $id the primary key of the model. + * @return \yii\db\ActiveRecordInterface the model being updated + * @throws \Exception if there is any error when updating the model + */ + public function run($id) + { + /** @var ActiveRecord $model */ + $model = $this->findModel($id); + + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id, $model); + } + + $model->scenario = $this->scenario; + $model->load(Yii::$app->getRequest()->getBodyParams(), ''); + + if ($this->transactional && $model instanceof ActiveRecord) { + if ($model->validate()) { + $transaction = $model->getDb()->beginTransaction(); + try { + $model->update(false); + $transaction->commit(); + } catch (\Exception $e) { + $transaction->rollback(); + throw $e; + } + } + } else { + $model->save(); + } + + return $model; + } +} diff --git a/framework/rest/UrlRule.php b/framework/rest/UrlRule.php new file mode 100644 index 0000000..5e4b218 --- /dev/null +++ b/framework/rest/UrlRule.php @@ -0,0 +1,250 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; +use yii\base\InvalidConfigException; +use yii\helpers\Inflector; +use yii\web\CompositeUrlRule; + +/** + * UrlRule is provided to simplify the creation of URL rules for RESTful API support. + * + * The simplest usage of UrlRule is to declare a rule like the following in the application configuration, + * + * ```php + * [ + * 'class' => 'yii\rest\UrlRule', + * 'controller' => 'user', + * ] + * ``` + * + * The above code will create a whole set of URL rules supporting the following RESTful API endpoints: + * + * - `'PUT,PATCH users/<id>' => 'user/update'`: update a user + * - `'DELETE users/<id>' => 'user/delete'`: delete a user + * - `'GET,HEAD users/<id>' => 'user/view'`: return the details/overview/options of a user + * - `'POST users' => 'user/create'`: create a new user + * - `'GET,HEAD users' => 'user/index'`: return a list/overview/options of users + * - `'users/<id>' => 'user/options'`: process all unhandled verbs of a user + * - `'users' => 'user/options'`: process all unhandled verbs of user collection + * + * You may configure [[only]] and/or [[except]] to disable some of the above rules. + * You may configure [[patterns]] to completely redefine your own list of rules. + * You may configure [[controller]] with multiple controller IDs to generate rules for all these controllers. + * For example, the following code will disable the `delete` rule and generate rules for both `user` and `post` controllers: + * + * ```php + * [ + * 'class' => 'yii\rest\UrlRule', + * 'controller' => ['user', 'post'], + * 'except' => ['delete'], + * ] + * ``` + * + * The property [[controller]] is required and should be the controller ID. It should be prefixed with + * the module ID if the controller is within a module. + * + * The controller ID used in the pattern will be automatically pluralized (e.g. `user` becomes `users` + * as shown in the above examples). You may configure [[urlName]] to explicitly specify the controller ID + * in the pattern. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class UrlRule extends CompositeUrlRule +{ + /** + * @var string the common prefix string shared by all patterns. + */ + public $prefix; + /** + * @var string the suffix that will be assigned to [[\yii\web\UrlRule::suffix]] for every generated rule. + */ + public $suffix; + /** + * @var string|array the controller ID (e.g. `user`, `post-comment`) that the rules in this composite rule + * are dealing with. It should be prefixed with the module ID if the controller is within a module (e.g. `admin/user`). + * + * By default, the controller ID will be pluralized automatically when it is put in the patterns of the + * generated rules. If you want to explicitly specify how the controller ID should appear in the patterns, + * you may use an array with the array key being as the controller ID in the pattern, and the array value + * the actual controller ID. For example, `['u' => 'user']`. + * + * You may also pass multiple controller IDs as an array. If this is the case, this composite rule will + * generate applicable URL rules for EVERY specified controller. For example, `['user', 'post']`. + */ + public $controller; + /** + * @var array list of acceptable actions. If not empty, only the actions within this array + * will have the corresponding URL rules created. + * @see patterns + */ + public $only = []; + /** + * @var array list of actions that should be excluded. Any action found in this array + * will NOT have its URL rules created. + * @see patterns + */ + public $except = []; + /** + * @var array patterns for supporting extra actions in addition to those listed in [[patterns]]. + * The keys are the patterns and the values are the corresponding action IDs. + * These extra patterns will take precedence over [[patterns]]. + */ + public $extraPatterns = []; + /** + * @var array list of tokens that should be replaced for each pattern. The keys are the token names, + * and the values are the corresponding replacements. + * @see patterns + */ + public $tokens = [ + '{id}' => '<id:\\d[\\d,]*>', + ]; + /** + * @var array list of possible patterns and the corresponding actions for creating the URL rules. + * The keys are the patterns and the values are the corresponding actions. + * The format of patterns is `Verbs Pattern`, where `Verbs` stands for a list of HTTP verbs separated + * by comma (without space). If `Verbs` is not specified, it means all verbs are allowed. + * `Pattern` is optional. It will be prefixed with [[prefix]]/[[controller]]/, + * and tokens in it will be replaced by [[tokens]]. + */ + public $patterns = [ + 'PUT,PATCH {id}' => 'update', + 'DELETE {id}' => 'delete', + 'GET,HEAD {id}' => 'view', + 'POST' => 'create', + 'GET,HEAD' => 'index', + '{id}' => 'options', + '' => 'options', + ]; + /** + * @var array the default configuration for creating each URL rule contained by this rule. + */ + public $ruleConfig = [ + 'class' => 'yii\web\UrlRule', + ]; + /** + * @var boolean whether to automatically pluralize the URL names for controllers. + * If true, a controller ID will appear in plural form in URLs. For example, `user` controller + * will appear as `users` in URLs. + * @see controllers + */ + public $pluralize = true; + + + /** + * @inheritdoc + */ + public function init() + { + if (empty($this->controller)) { + throw new InvalidConfigException('"controller" must be set.'); + } + + $controllers = []; + foreach ((array)$this->controller as $urlName => $controller) { + if (is_integer($urlName)) { + $urlName = $this->pluralize ? Inflector::pluralize($controller) : $controller; + } + $controllers[$urlName] = $controller; + } + $this->controller = $controllers; + + $this->prefix = trim($this->prefix, '/'); + + parent::init(); + } + + /** + * @inheritdoc + */ + protected function createRules() + { + $only = array_flip($this->only); + $except = array_flip($this->except); + $patterns = array_merge($this->patterns, $this->extraPatterns); + $rules = []; + foreach ($this->controller as $urlName => $controller) { + $prefix = trim($this->prefix . '/' . $urlName, '/'); + foreach ($patterns as $pattern => $action) { + if (!isset($except[$action]) && (empty($only) || isset($only[$action]))) { + $rules[$urlName][] = $this->createRule($pattern, $prefix, $controller . '/' . $action); + } + } + } + return $rules; + } + + /** + * Creates a URL rule using the given pattern and action. + * @param string $pattern + * @param string $prefix + * @param string $action + * @return \yii\web\UrlRuleInterface + */ + protected function createRule($pattern, $prefix, $action) + { + $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS'; + if (preg_match("/^((?:($verbs),)*($verbs))(?:\\s+(.*))?$/", $pattern, $matches)) { + $verbs = explode(',', $matches[1]); + $pattern = isset($matches[4]) ? $matches[4] : ''; + } else { + $verbs = []; + } + + $config = $this->ruleConfig; + $config['verb'] = $verbs; + $config['pattern'] = rtrim($prefix . '/' . strtr($pattern, $this->tokens), '/'); + $config['route'] = $action; + if (!in_array('GET', $verbs)) { + $config['mode'] = \yii\web\UrlRule::PARSING_ONLY; + } + $config['suffix'] = $this->suffix; + + return Yii::createObject($config); + } + + /** + * @inheritdoc + */ + public function parseRequest($manager, $request) + { + $pathInfo = $request->getPathInfo(); + foreach ($this->rules as $urlName => $rules) { + if (strpos($pathInfo, $urlName) !== false) { + foreach ($rules as $rule) { + /** @var \yii\web\UrlRule $rule */ + if (($result = $rule->parseRequest($manager, $request)) !== false) { + Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); + return $result; + } + } + } + } + return false; + } + + /** + * @inheritdoc + */ + public function createUrl($manager, $route, $params) + { + foreach ($this->controller as $urlName => $controller) { + if (strpos($route, $controller) !== false) { + foreach ($this->rules[$urlName] as $rule) { + /** @var \yii\web\UrlRule $rule */ + if (($url = $rule->createUrl($manager, $route, $params)) !== false) { + return $url; + } + } + } + } + return false; + } +} diff --git a/framework/rest/ViewAction.php b/framework/rest/ViewAction.php new file mode 100644 index 0000000..c37522f --- /dev/null +++ b/framework/rest/ViewAction.php @@ -0,0 +1,33 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\rest; + +use Yii; + +/** + * ViewAction implements the API endpoint for returning the detailed information about a model. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class ViewAction extends Action +{ + /** + * Displays a model. + * @param string $id the primary key of the model. + * @return \yii\db\ActiveRecordInterface the model being displayed + */ + public function run($id) + { + $model = $this->findModel($id); + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id, $model); + } + return $model; + } +} diff --git a/framework/test/Fixture.php b/framework/test/Fixture.php index 5a38ae5..e22a139 100644 --- a/framework/test/Fixture.php +++ b/framework/test/Fixture.php @@ -82,4 +82,3 @@ class Fixture extends Component { } } - diff --git a/framework/validators/ImageValidator.php b/framework/validators/ImageValidator.php index 52fb235..fa9587d 100644 --- a/framework/validators/ImageValidator.php +++ b/framework/validators/ImageValidator.php @@ -124,7 +124,7 @@ class ImageValidator extends FileValidator } if ($this->underHeight === null) { $this->underHeight = Yii::t('yii', 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); - } + } if ($this->overWidth === null) { $this->overWidth = Yii::t('yii', 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); } diff --git a/framework/validators/PunycodeAsset.php b/framework/validators/PunycodeAsset.php index c0c1e2b..5f6a411 100644 --- a/framework/validators/PunycodeAsset.php +++ b/framework/validators/PunycodeAsset.php @@ -6,6 +6,7 @@ */ namespace yii\validators; + use yii\web\AssetBundle; /** diff --git a/framework/validators/ValidationAsset.php b/framework/validators/ValidationAsset.php index 14d7ad0..e9bb79d 100644 --- a/framework/validators/ValidationAsset.php +++ b/framework/validators/ValidationAsset.php @@ -6,6 +6,7 @@ */ namespace yii\validators; + use yii\web\AssetBundle; /** diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index 764de66..fac3706 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -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; diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index c634755..8fcdf2c 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -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) { diff --git a/framework/web/AssetConverter.php b/framework/web/AssetConverter.php index 1b7d1c8..23062d9 100644 --- a/framework/web/AssetConverter.php +++ b/framework/web/AssetConverter.php @@ -82,7 +82,7 @@ class AssetConverter extends Component implements AssetConverterInterface $proc = proc_open($command, $descriptor, $pipes, $basePath); $stdout = stream_get_contents($pipes[1]); $stderr = stream_get_contents($pipes[2]); - foreach($pipes as $pipe) { + foreach ($pipes as $pipe) { fclose($pipe); } $status = proc_close($proc); diff --git a/framework/web/CompositeUrlRule.php b/framework/web/CompositeUrlRule.php new file mode 100644 index 0000000..2382ec5 --- /dev/null +++ b/framework/web/CompositeUrlRule.php @@ -0,0 +1,73 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use Yii; +use yii\base\Object; + +/** + * CompositeUrlRule represents a collection of related URL rules. + * + * These URL rules are typically created for a common purpose (e.g. RESTful API for a resource). + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +abstract class CompositeUrlRule extends Object implements UrlRuleInterface +{ + /** + * @var UrlRuleInterface[] the URL rules contained in this composite rule. + * This property is set in [[init()]] by the return value of [[createRules()]]. + */ + protected $rules = []; + + + /** + * Creates the URL rules that should be contained within this composite rule. + * @return UrlRuleInterface[] the URL rules + */ + abstract protected function createRules(); + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + $this->rules = $this->createRules(); + } + + /** + * @inheritdoc + */ + public function parseRequest($manager, $request) + { + foreach ($this->rules as $rule) { + /** @var \yii\web\UrlRule $rule */ + if (($result = $rule->parseRequest($manager, $request)) !== false) { + Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); + return $result; + } + } + return false; + } + + /** + * @inheritdoc + */ + public function createUrl($manager, $route, $params) + { + foreach ($this->rules as $rule) { + /** @var \yii\web\UrlRule $rule */ + if (($url = $rule->createUrl($manager, $route, $params)) !== false) { + return $url; + } + } + return false; + } +} diff --git a/framework/web/IdentityInterface.php b/framework/web/IdentityInterface.php index c796b50..2aac17f 100644 --- a/framework/web/IdentityInterface.php +++ b/framework/web/IdentityInterface.php @@ -52,6 +52,14 @@ interface IdentityInterface */ public static function findIdentity($id); /** + * Finds an identity by the given secrete token. + * @param string $token the secrete token + * @return IdentityInterface the identity object that matches the given token. + * Null should be returned if such an identity cannot be found + * or the identity is not in an active state (disabled, deleted, etc.) + */ + public static function findIdentityByAccessToken($token); + /** * Returns an ID that can uniquely identify a user identity. * @return string|integer an ID that uniquely identifies a user identity. */ diff --git a/framework/web/Link.php b/framework/web/Link.php new file mode 100644 index 0000000..9e10e9b --- /dev/null +++ b/framework/web/Link.php @@ -0,0 +1,83 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use yii\base\Arrayable; +use yii\base\Object; + +/** + * Link represents a link object as defined in [JSON Hypermedia API Language](https://tools.ietf.org/html/draft-kelly-json-hal-03). + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +class Link extends Object implements Arrayable +{ + /** + * The self link. + */ + const REL_SELF = 'self'; + + /** + * @var string a URI [RFC3986](https://tools.ietf.org/html/rfc3986) or + * URI template [RFC6570](https://tools.ietf.org/html/rfc6570). This property is required. + */ + public $href; + /** + * @var string a secondary key for selecting Link Objects which share the same relation type + */ + public $name; + /** + * @var string a hint to indicate the media type expected when dereferencing the target resource + */ + public $type; + /** + * @var boolean a value indicating whether [[href]] refers to a URI or URI template. + */ + public $templated = false; + /** + * @var string a URI that hints about the profile of the target resource. + */ + public $profile; + /** + * @var string a label describing the link + */ + public $title; + /** + * @var string the language of the target resource + */ + public $hreflang; + + /** + * @inheritdoc + */ + public function toArray() + { + return array_filter((array)$this); + } + + /** + * Serializes a list of links into proper array format. + * @param array $links the links to be serialized + * @return array the proper array representation of the links. + */ + public static function serialize(array $links) + { + foreach ($links as $rel => $link) { + if (is_array($link)) { + foreach ($link as $i => $l) { + $link[$i] = $l instanceof self ? $l->toArray() : ['href' => $l]; + } + $links[$rel] = $link; + } elseif (!$link instanceof self) { + $links[$rel] = ['href' => $link]; + } + } + return $links; + } +} diff --git a/framework/web/Linkable.php b/framework/web/Linkable.php new file mode 100644 index 0000000..8d1558b --- /dev/null +++ b/framework/web/Linkable.php @@ -0,0 +1,42 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +/** + * Linkable is the interface that should be implemented by classes that typically represent locatable resources. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +interface Linkable +{ + /** + * Returns a list of links. + * + * Each link is either a URI or a [[Link]] object. The return value of this method should + * be an array whose keys are the relation names and values the corresponding links. + * + * If a relation name corresponds to multiple links, use an array to represent them. + * + * For example, + * + * ```php + * [ + * 'self' => 'http://example.com/users/1', + * 'friends' => [ + * 'http://example.com/users/2', + * 'http://example.com/users/3', + * ], + * 'manager' => $managerLink, // $managerLink is a Link object + * ] + * ``` + * + * @return array the links + */ + public function getLinks(); +} diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php index 4c8cc50..08857d5 100644 --- a/framework/web/PageCache.php +++ b/framework/web/PageCache.php @@ -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(); } } diff --git a/framework/web/Request.php b/framework/web/Request.php index 01de665..0f9a211 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -819,6 +819,22 @@ class Request extends \yii\base\Request return isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null; } + /** + * @return string the username sent via HTTP authentication, null if the username is not given + */ + public function getAuthUser() + { + return isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null; + } + + /** + * @return string the password sent via HTTP authentication, null if the password is not given + */ + public function getAuthPassword() + { + return isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null; + } + private $_port; /** @@ -885,9 +901,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() { @@ -902,8 +932,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) { @@ -937,7 +971,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 = []; } @@ -956,45 +990,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; } /** diff --git a/framework/web/Session.php b/framework/web/Session.php index 407bfbf..b43b882 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -75,10 +75,6 @@ use yii\base\InvalidParamException; class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Countable, Arrayable { /** - * @var boolean whether the session should be automatically started when the session component is initialized. - */ - public $autoStart = true; - /** * @var string the name of the session variable that stores the flash message data. */ public $flashParam = '__flash'; @@ -100,9 +96,6 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co public function init() { parent::init(); - if ($this->autoStart) { - $this->open(); - } register_shutdown_function([$this, 'close']); } @@ -123,10 +116,32 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function open() { - if (session_status() == PHP_SESSION_ACTIVE) { + if ($this->getIsActive()) { return; } + $this->registerSessionHandler(); + + $this->setCookieParamsInternal(); + + @session_start(); + + if ($this->getIsActive()) { + Yii::info('Session started', __METHOD__); + $this->updateFlashCounters(); + } else { + $error = error_get_last(); + $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; + Yii::error($message, __METHOD__); + } + } + + /** + * Registers session handler. + * @throws \yii\base\InvalidConfigException + */ + protected function registerSessionHandler() + { if ($this->handler !== null) { if (!is_object($this->handler)) { $this->handler = Yii::createObject($this->handler); @@ -145,18 +160,6 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co [$this, 'gcSession'] ); } - - $this->setCookieParamsInternal(); - - @session_start(); - - if (session_id() == '') { - $error = error_get_last(); - $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; - Yii::error($message, __METHOD__); - } else { - $this->updateFlashCounters(); - } } /** @@ -164,7 +167,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function close() { - if (session_id() !== '') { + if ($this->getIsActive()) { @session_write_close(); } } @@ -174,7 +177,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function destroy() { - if (session_id() !== '') { + if ($this->getIsActive()) { @session_unset(); @session_destroy(); } @@ -188,6 +191,42 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co return session_status() == PHP_SESSION_ACTIVE; } + private $_hasSessionId; + + /** + * Returns a value indicating whether the current request has sent the session ID. + * The default implementation will check cookie and $_GET using the session name. + * If you send session ID via other ways, you may need to override this method + * or call [[setHasSessionId()]] to explicitly set whether the session ID is sent. + * @return boolean whether the current request has sent the session ID. + */ + public function getHasSessionId() + { + if ($this->_hasSessionId === null) { + $name = $this->getName(); + $request = Yii::$app->getRequest(); + if (ini_get('session.use_cookies') && !empty($_COOKIE[$name])) { + $this->_hasSessionId = true; + } elseif (!ini_get('use_only_cookies') && ini_get('use_trans_sid')) { + $this->_hasSessionId = $request->get($name) !== null; + } else { + $this->_hasSessionId = false; + } + } + return $this->_hasSessionId; + } + + /** + * Sets the value indicating whether the current request has sent the session ID. + * This method is provided so that you can override the default way of determining + * whether the session ID is sent. + * @param boolean $value whether the current request has sent the session ID. + */ + public function setHasSessionId($value) + { + $this->_hasSessionId = $value; + } + /** * @return string the current session ID */ @@ -506,6 +545,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function get($key, $defaultValue = null) { + $this->open(); return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue; } @@ -517,6 +557,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function set($key, $value) { + $this->open(); $_SESSION[$key] = $value; } @@ -527,6 +568,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function remove($key) { + $this->open(); if (isset($_SESSION[$key])) { $value = $_SESSION[$key]; unset($_SESSION[$key]); @@ -541,6 +583,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function removeAll() { + $this->open(); foreach (array_keys($_SESSION) as $key) { unset($_SESSION[$key]); } @@ -552,6 +595,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function has($key) { + $this->open(); return isset($_SESSION[$key]); } @@ -560,6 +604,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function toArray() { + $this->open(); return $_SESSION; } @@ -689,6 +734,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function offsetExists($offset) { + $this->open(); return isset($_SESSION[$offset]); } @@ -699,6 +745,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function offsetGet($offset) { + $this->open(); return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null; } @@ -709,6 +756,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function offsetSet($offset, $item) { + $this->open(); $_SESSION[$offset] = $item; } @@ -718,6 +766,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function offsetUnset($offset) { + $this->open(); unset($_SESSION[$offset]); } } diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 80f2cb6..85709c9 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -9,6 +9,7 @@ namespace yii\web; use Yii; use yii\base\Component; +use yii\base\InvalidConfigException; use yii\caching\Cache; /** @@ -156,17 +157,22 @@ class UrlManager extends Component } $rules = []; + $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS'; foreach ($this->rules as $key => $rule) { if (!is_array($rule)) { $rule = ['route' => $rule]; - if (preg_match('/^((?:(GET|HEAD|POST|PUT|PATCH|DELETE),)*(GET|HEAD|POST|PUT|PATCH|DELETE))\s+(.*)$/', $key, $matches)) { + if (preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches)) { $rule['verb'] = explode(',', $matches[1]); $rule['mode'] = UrlRule::PARSING_ONLY; $key = $matches[4]; } $rule['pattern'] = $key; } - $rules[] = Yii::createObject(array_merge($this->ruleConfig, $rule)); + $rule = Yii::createObject(array_merge($this->ruleConfig, $rule)); + if (!$rule instanceof UrlRuleInterface) { + throw new InvalidConfigException('URL rule class must implement UrlRuleInterface.'); + } + $rules[] = $rule; } $this->rules = $rules; @@ -188,7 +194,6 @@ class UrlManager extends Component /** @var UrlRule $rule */ foreach ($this->rules as $rule) { if (($result = $rule->parseRequest($this, $request)) !== false) { - Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); return $result; } } @@ -245,7 +250,7 @@ class UrlManager extends Component /** @var UrlRule $rule */ foreach ($this->rules as $rule) { if (($url = $rule->createUrl($this, $route, $params)) !== false) { - if ($rule->host !== null) { + if (strpos($url, '://') !== false) { if ($baseUrl !== '' && ($pos = strpos($url, '/', 8)) !== false) { return substr($url, 0, $pos) . $baseUrl . substr($url, $pos); } else { diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 5a8c3f9..42c9097 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -7,6 +7,7 @@ namespace yii\web; +use Yii; use yii\base\Object; use yii\base\InvalidConfigException; @@ -26,7 +27,7 @@ use yii\base\InvalidConfigException; * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ -class UrlRule extends Object +class UrlRule extends Object implements UrlRuleInterface { /** * Set [[mode]] with this value to mark that this rule is for URL parsing only @@ -47,7 +48,7 @@ class UrlRule extends Object */ public $pattern; /** - * @var string the pattern used to parse and create the host info part of a URL. + * @var string the pattern used to parse and create the host info part of a URL (e.g. `http://example.com`). * @see pattern */ public $host; @@ -127,7 +128,8 @@ class UrlRule extends Object $this->pattern = trim($this->pattern, '/'); if ($this->host !== null) { - $this->pattern = rtrim($this->host, '/') . rtrim('/' . $this->pattern, '/') . '/'; + $this->host = rtrim($this->host, '/'); + $this->pattern = rtrim($this->host . '/' . $this->pattern, '/'); } elseif ($this->pattern === '') { $this->_template = ''; $this->pattern = '#^$#u'; @@ -157,7 +159,7 @@ class UrlRule extends Object foreach ($matches as $match) { $name = $match[1][0]; $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+'; - if (isset($this->defaults[$name])) { + if (array_key_exists($name, $this->defaults)) { $length = strlen($match[0][0]); $offset = $match[0][1]; if ($offset > 1 && $this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { @@ -197,7 +199,7 @@ class UrlRule extends Object return false; } - if ($this->verb !== null && !in_array($request->getMethod(), $this->verb, true)) { + if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) { return false; } @@ -243,6 +245,9 @@ class UrlRule extends Object } else { $route = $this->route; } + + Yii::trace("Request parsed with URL rule: {$this->name}", __METHOD__); + return [$route, $params]; } diff --git a/framework/web/UrlRuleInterface.php b/framework/web/UrlRuleInterface.php new file mode 100644 index 0000000..e6a5385 --- /dev/null +++ b/framework/web/UrlRuleInterface.php @@ -0,0 +1,34 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +/** + * UrlRuleInterface is the interface that should be implemented URL rule classes. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +interface UrlRuleInterface +{ + /** + * Parses the given request and returns the corresponding route and parameters. + * @param UrlManager $manager the URL manager + * @param Request $request the request component + * @return array|boolean the parsing result. The route and the parameters are returned as an array. + * If false, it means this rule cannot be used to parse this path info. + */ + public function parseRequest($manager, $request); + /** + * Creates a URL according to the given route and parameters. + * @param UrlManager $manager the URL manager + * @param string $route the route. It should not have slashes at the beginning or the end. + * @param array $params the parameters + * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL. + */ + public function createUrl($manager, $route, $params); +} diff --git a/framework/web/User.php b/framework/web/User.php index 927d672..c32ae9c 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -14,11 +14,20 @@ use yii\base\InvalidConfigException; /** * User is the class for the "user" application component that manages the user authentication status. * - * In particular, [[User::isGuest]] returns a value indicating whether the current user is a guest or not. - * Through methods [[login()]] and [[logout()]], you can change the user authentication status. + * You may use [[isGuest]] to determine whether the current user is a guest or not. + * If the user is a guest, the [[identity]] property would return null. Otherwise, it would + * be an instance of [[IdentityInterface]]. * - * User works with a class implementing the [[IdentityInterface]]. This class implements - * the actual user authentication logic and is often backed by a user database table. + * You may call various methods to change the user authentication status: + * + * - [[login()]]: sets the specified identity and remembers the authentication status in session and cookie. + * - [[logout()]]: marks the user as a guest and clears the relevant information from session and cookie. + * - [[setIdentity()]]: changes the user identity without touching session or cookie. + * This is best used in stateless RESTful API implementation. + * + * Note that User only maintains the user authentication status. It does NOT handle how to authenticate + * a user. The logic of how to authenticate a user should be done in the class implementing [[IdentityInterface]]. + * You are also required to set [[identityClass]] with the name of this class. * * User is configured as an application component in [[\yii\web\Application]] by default. * You can access that instance via `Yii::$app->user`. @@ -124,66 +133,64 @@ class User extends Component if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); } - - Yii::$app->getSession()->open(); - - $this->renewAuthStatus(); - - if ($this->enableAutoLogin) { - if ($this->getIsGuest()) { - $this->loginByCookie(); - } elseif ($this->autoRenewCookie) { - $this->renewIdentityCookie(); - } - } } private $_identity = false; /** - * Returns the identity object associated with the currently logged user. - * @return IdentityInterface the identity object associated with the currently logged user. + * Returns the identity object associated with the currently logged-in user. + * @param boolean $checkSession whether to check the session if the identity has never been determined before. + * If the identity is already determined (e.g., by calling [[setIdentity()]] or [[login()]]), + * then this parameter has no effect. + * @return IdentityInterface the identity object associated with the currently logged-in user. * Null is returned if the user is not logged in (not authenticated). * @see login() * @see logout() */ - public function getIdentity() + public function getIdentity($checkSession = true) { if ($this->_identity === false) { - $id = $this->getId(); - if ($id === null) { - $this->_identity = null; + if ($checkSession) { + $this->renewAuthStatus(); } else { - /** @var IdentityInterface $class */ - $class = $this->identityClass; - $this->_identity = $class::findIdentity($id); + return null; } } return $this->_identity; } /** - * Sets the identity object. - * This method should be mainly be used by the User component or its child class - * to maintain the identity object. + * Sets the user identity object. + * + * This method does nothing else except storing the specified identity object in the internal variable. + * For this reason, this method is best used when the user authentication status should not be maintained + * by session. * - * You should normally update the user identity via methods [[login()]], [[logout()]] - * or [[switchIdentity()]]. + * This method is also called by other more sophisticated methods, such as [[login()]], [[logout()]], + * [[switchIdentity()]]. Those methods will try to use session and cookie to maintain the user authentication + * status. * * @param IdentityInterface $identity the identity object associated with the currently logged user. */ public function setIdentity($identity) { $this->_identity = $identity; + $this->_access = []; } /** * Logs in a user. * - * This method stores the necessary session information to keep track - * of the user identity information. If `$duration` is greater than 0 - * and [[enableAutoLogin]] is true, it will also send out an identity - * cookie to support cookie-based login. + * By logging in a user, you may obtain the user identity information each time through [[identity]]. + * + * The login status is maintained according to the `$duration` parameter: + * + * - `$duration == 0`: the identity information will be stored in session and will be available + * via [[identity]] as long as the session remains active. + * - `$duration > 0`: the identity information will be stored in session. If [[enableAutoLogin]] is true, + * it will also be stored in a cookie which will expire in `$duration` seconds. As long as + * the cookie remains valid or the session is active, you may obtain the user identity information + * via [[identity]]. * * @param IdentityInterface $identity the user identity (which should already be authenticated) * @param integer $duration number of seconds that the user can remain in logged-in status. @@ -193,17 +200,34 @@ class User extends Component */ public function login($identity, $duration = 0) { - if ($this->beforeLogin($identity, false)) { + if ($this->beforeLogin($identity, false, $duration)) { $this->switchIdentity($identity, $duration); $id = $identity->getId(); $ip = Yii::$app->getRequest()->getUserIP(); - Yii::info("User '$id' logged in from $ip.", __METHOD__); - $this->afterLogin($identity, false); + Yii::info("User '$id' logged in from $ip with duration $duration.", __METHOD__); + $this->afterLogin($identity, false, $duration); } return !$this->getIsGuest(); } /** + * Logs in a user by the given access token. + * Note that unlike [[login()]], this method will NOT start a session to remember the user authentication status. + * Also if the access token is invalid, the user will remain as a guest. + * @param string $token the access token + * @return IdentityInterface the identity associated with the given access token. Null is returned if + * the access token is invalid. + */ + public function loginByAccessToken($token) + { + /** @var IdentityInterface $class */ + $class = $this->identityClass; + $identity = $class::findIdentityByAccessToken($token); + $this->setIdentity($identity); + return $identity; + } + + /** * Logs in a user by cookie. * * This method attempts to log in a user using the ID and authKey information @@ -221,11 +245,11 @@ class User extends Component $class = $this->identityClass; $identity = $class::findIdentity($id); if ($identity !== null && $identity->validateAuthKey($authKey)) { - if ($this->beforeLogin($identity, true)) { + if ($this->beforeLogin($identity, true, $duration)) { $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); $ip = Yii::$app->getRequest()->getUserIP(); Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__); - $this->afterLogin($identity, true); + $this->afterLogin($identity, true, $duration); } } elseif ($identity !== null) { Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); @@ -270,7 +294,8 @@ class User extends Component */ public function getId() { - return Yii::$app->getSession()->get($this->idParam); + $identity = $this->getIdentity(); + return $identity !== null ? $identity->getId() : null; } /** @@ -345,13 +370,16 @@ class User extends Component * so that the event is triggered. * @param IdentityInterface $identity the user identity information * @param boolean $cookieBased whether the login is cookie-based + * @param integer $duration number of seconds that the user can remain in logged-in status. + * If 0, it means login till the user closes the browser or the session is manually destroyed. * @return boolean whether the user should continue to be logged in */ - protected function beforeLogin($identity, $cookieBased) + protected function beforeLogin($identity, $cookieBased, $duration) { $event = new UserEvent([ 'identity' => $identity, 'cookieBased' => $cookieBased, + 'duration' => $duration, ]); $this->trigger(self::EVENT_BEFORE_LOGIN, $event); return $event->isValid; @@ -364,12 +392,15 @@ class User extends Component * so that the event is triggered. * @param IdentityInterface $identity the user identity information * @param boolean $cookieBased whether the login is cookie-based + * @param integer $duration number of seconds that the user can remain in logged-in status. + * If 0, it means login till the user closes the browser or the session is manually destroyed. */ - protected function afterLogin($identity, $cookieBased) + protected function afterLogin($identity, $cookieBased, $duration) { $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent([ 'identity' => $identity, 'cookieBased' => $cookieBased, + 'duration' => $duration, ])); } @@ -448,15 +479,14 @@ class User extends Component /** * Switches to a new identity for the current user. * - * This method will save necessary session information to keep track of the user authentication status. - * If `$duration` is provided, it will also send out appropriate identity cookie - * to support cookie-based login. + * This method may use session and/or cookie to store the user identity information, + * according to the value of `$duration`. Please refer to [[login()]] for more details. * * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]] * when the current user needs to be associated with the corresponding identity information. * * @param IdentityInterface $identity the identity information to be associated with the current user. - * If null, it means switching to be a guest. + * If null, it means switching the current user to be a guest. * @param integer $duration number of seconds that the user can remain in logged-in status. * This parameter is used only when `$identity` is not null. */ @@ -483,19 +513,44 @@ class User extends Component } /** - * Updates the authentication status according to [[authTimeout]]. - * This method is called during [[init()]]. - * It will update the user's authentication status if it has not outdated yet. - * Otherwise, it will logout the user. + * Updates the authentication status using the information from session and cookie. + * + * This method will try to determine the user identity using the [[idParam]] session variable. + * + * If [[authTimeout]] is set, this method will refresh the timer. + * + * If the user identity cannot be determined by session, this method will try to [[loginByCookie()|login by cookie]] + * if [[enableAutoLogin]] is true. */ protected function renewAuthStatus() { - if ($this->authTimeout !== null && !$this->getIsGuest()) { - $expire = Yii::$app->getSession()->get($this->authTimeoutParam); + $session = Yii::$app->getSession(); + $id = $session->getHasSessionId() ? $session->get($this->idParam) : null; + + if ($id === null) { + $identity = null; + } else { + /** @var IdentityInterface $class */ + $class = $this->identityClass; + $identity = $class::findIdentity($id); + } + + $this->setIdentity($identity); + + if ($this->authTimeout !== null && $identity !== null) { + $expire = $session->get($this->authTimeoutParam); if ($expire !== null && $expire < time()) { $this->logout(false); } else { - Yii::$app->getSession()->set($this->authTimeoutParam, time() + $this->authTimeout); + $session->set($this->authTimeoutParam, time() + $this->authTimeout); + } + } + + if ($this->enableAutoLogin) { + if ($this->getIsGuest()) { + $this->loginByCookie(); + } elseif ($this->autoRenewCookie) { + $this->renewIdentityCookie(); } } } diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php index 8577ef5..bc6d5fe 100644 --- a/framework/web/UserEvent.php +++ b/framework/web/UserEvent.php @@ -27,6 +27,11 @@ class UserEvent extends Event */ public $cookieBased; /** + * @var integer $duration number of seconds that the user can remain in logged-in status. + * If 0, it means login till the user closes the browser or the session is manually destroyed. + */ + public $duration; + /** * @var boolean whether the login or logout should proceed. * Event handlers may modify this property to determine whether the login or logout should proceed. * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. diff --git a/framework/widgets/ActiveFormAsset.php b/framework/widgets/ActiveFormAsset.php index 5acb5e1..94b00e5 100644 --- a/framework/widgets/ActiveFormAsset.php +++ b/framework/widgets/ActiveFormAsset.php @@ -6,6 +6,7 @@ */ namespace yii\widgets; + use yii\web\AssetBundle; /** diff --git a/framework/widgets/DetailView.php b/framework/widgets/DetailView.php index 40ea29f..4ba6206 100644 --- a/framework/widgets/DetailView.php +++ b/framework/widgets/DetailView.php @@ -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; diff --git a/framework/widgets/LinkPager.php b/framework/widgets/LinkPager.php index 06d688a..22cfdf0 100644 --- a/framework/widgets/LinkPager.php +++ b/framework/widgets/LinkPager.php @@ -89,6 +89,13 @@ class LinkPager extends Widget * If this property is null, the "last" page button will not be displayed. */ public $lastPageLabel; + /** + * @var bool whether to register link tags in the HTML header for prev, next, first and last page. + * Defaults to `false` to avoid conflicts when multiple pagers are used on one page. + * @see http://www.w3.org/TR/html401/struct/links.html#h-12.1.2 + * @see registerLinkTags() + */ + public $registerLinkTags = false; /** @@ -107,10 +114,26 @@ class LinkPager extends Widget */ public function run() { + if ($this->registerLinkTags) { + $this->registerLinkTags(); + } echo $this->renderPageButtons(); } /** + * Registers relational link tags in the html header for prev, next, first and last page. + * These links are generated using [[yii\data\Pagination::getLinks()]]. + * @see http://www.w3.org/TR/html401/struct/links.html#h-12.1.2 + */ + protected function registerLinkTags() + { + $view = $this->getView(); + foreach ($this->pagination->getLinks() as $rel => $href) { + $view->registerLinkTag(['rel' => $rel, 'href' => $href], $rel); + } + } + + /** * Renders the page buttons. * @return string the rendering result */ diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index 1d7046c..5a60498 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -14,7 +14,7 @@ use yii\helpers\Json; use yii\web\Response; /** - * Pjax is a widget integrating the [pjax](https://github.com/defunkt/jquery-pjax) jQuery plugin. + * Pjax is a widget integrating the [pjax](https://github.com/yiisoft/jquery-pjax) jQuery plugin. * * Pjax only deals with the content enclosed between its [[begin()]] and [[end()]] calls, called the *body content* of the widget. * By default, any link click or form submission (for those forms with `data-pjax` attribute) within the body content @@ -25,7 +25,9 @@ use yii\web\Response; * You may configure [[linkSelector]] to specify which links should trigger pjax, and configure [[formSelector]] * to specify which form submission may trigger pjax. * - * The following example shows how to use Pjax with the [[\yii\gridview\GridView]] widget so that the grid pagination, + * You may disable pjax for a specific link inside the container by adding `data-pjax="0"` attribute to this link. + * + * The following example shows how to use Pjax with the [[\yii\grid\GridView]] widget so that the grid pagination, * sorting and filtering can be done via pjax: * * ```php @@ -78,7 +80,7 @@ class Pjax extends Widget public $scrollTo = false; /** * @var array additional options to be passed to the pjax JS plugin. Please refer to - * [pjax project page](https://github.com/defunkt/jquery-pjax) for available options. + * [pjax project page](https://github.com/yiisoft/jquery-pjax) for available options. */ public $clientOptions; diff --git a/framework/widgets/PjaxAsset.php b/framework/widgets/PjaxAsset.php index f3e4912..3fcaf67 100644 --- a/framework/widgets/PjaxAsset.php +++ b/framework/widgets/PjaxAsset.php @@ -17,9 +17,9 @@ use yii\web\AssetBundle; */ class PjaxAsset extends AssetBundle { - public $sourcePath = '@yii/assets'; + public $sourcePath = '@vendor/yiisoft/jquery-pjax'; public $js = [ - 'pjax/jquery.pjax.js', + 'jquery.pjax.js', ]; public $depends = [ 'yii\web\YiiAsset', diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index 3a64e61..bac3850 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -12,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 { @@ -25,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'); diff --git a/tests/unit/data/ar/Profile.php b/tests/unit/data/ar/Profile.php new file mode 100644 index 0000000..7651d88 --- /dev/null +++ b/tests/unit/data/ar/Profile.php @@ -0,0 +1,22 @@ +<?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 diff --git a/tests/unit/data/ar/redis/Customer.php b/tests/unit/data/ar/redis/Customer.php index 4f3a257..06ad93a 100644 --- a/tests/unit/data/ar/redis/Customer.php +++ b/tests/unit/data/ar/redis/Customer.php @@ -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 +} diff --git a/tests/unit/data/cubrid.sql b/tests/unit/data/cubrid.sql index 1af3c5c..cecc66d 100644 --- a/tests/unit/data/cubrid.sql +++ b/tests/unit/data/cubrid.sql @@ -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'); diff --git a/tests/unit/data/mssql.sql b/tests/unit/data/mssql.sql index e25dbbb..0b9dfaa 100644 --- a/tests/unit/data/mssql.sql +++ b/tests/unit/data/mssql.sql @@ -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'); diff --git a/tests/unit/data/mysql.sql b/tests/unit/data/mysql.sql index 71d6159..f2e1e77 100644 --- a/tests/unit/data/mysql.sql +++ b/tests/unit/data/mysql.sql @@ -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'); diff --git a/tests/unit/data/postgres.sql b/tests/unit/data/postgres.sql index 2d8737b..32557b2 100644 --- a/tests/unit/data/postgres.sql +++ b/tests/unit/data/postgres.sql @@ -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'); diff --git a/tests/unit/data/sqlite.sql b/tests/unit/data/sqlite.sql index fcdb68c..8d103d6 100644 --- a/tests/unit/data/sqlite.sql +++ b/tests/unit/data/sqlite.sql @@ -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'); diff --git a/tests/unit/data/travis/cubrid-setup.sh b/tests/unit/data/travis/cubrid-setup.sh index 9c3bb74..4188e17 100755 --- a/tests/unit/data/travis/cubrid-setup.sh +++ b/tests/unit/data/travis/cubrid-setup.sh @@ -2,6 +2,11 @@ # # install CUBRID DBMS +if (php --version | grep -i HHVM > /dev/null); then + echo "Skipping CUBRID on HHVM" + exit 0 +fi + # cubrid dbms echo 'yes' | sudo add-apt-repository ppa:cubrid/cubrid sudo apt-get update diff --git a/tests/unit/data/travis/memcache-setup.sh b/tests/unit/data/travis/memcache-setup.sh index 6b623d6..adb743b 100755 --- a/tests/unit/data/travis/memcache-setup.sh +++ b/tests/unit/data/travis/memcache-setup.sh @@ -1,4 +1,8 @@ #!/bin/sh -echo "extension=memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini -echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini +if (php --version | grep -i HHVM > /dev/null); then + echo "skipping memcache on HHVM" +else + echo "extension=memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini +fi diff --git a/tests/unit/extensions/mongodb/CollectionTest.php b/tests/unit/extensions/mongodb/CollectionTest.php index 3eb97b6..9a0c0b4 100644 --- a/tests/unit/extensions/mongodb/CollectionTest.php +++ b/tests/unit/extensions/mongodb/CollectionTest.php @@ -187,6 +187,60 @@ class CollectionTest extends MongoDbTestCase $this->assertNotEmpty($result[0]['items']); } + public function testFindAndModify() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = [ + [ + 'name' => 'customer 1', + 'status' => 1, + 'amount' => 100, + ], + [ + 'name' => 'customer 2', + 'status' => 1, + 'amount' => 200, + ], + ]; + $collection->batchInsert($rows); + + // increment field + $result = $collection->findAndModify(['name' => 'customer 1'], ['$inc' => ['status' => 1]]); + $this->assertEquals('customer 1', $result['name']); + $this->assertEquals(1, $result['status']); + $newResult = $collection->findOne(['name' => 'customer 1']); + $this->assertEquals(2, $newResult['status']); + + // $set and return modified document + $result = $collection->findAndModify( + ['name' => 'customer 2'], + ['$set' => ['status' => 2]], + [], + ['new' => true] + ); + $this->assertEquals('customer 2', $result['name']); + $this->assertEquals(2, $result['status']); + + // Full update document + $data = [ + 'name' => 'customer 3', + 'city' => 'Minsk' + ]; + $result = $collection->findAndModify( + ['name' => 'customer 2'], + $data, + [], + ['new' => true] + ); + $this->assertEquals('customer 3', $result['name']); + $this->assertEquals('Minsk', $result['city']); + $this->assertTrue(!isset($result['status'])); + + // Test exceptions + $this->setExpectedException('\yii\mongodb\Exception'); + $collection->findAndModify(['name' => 'customer 1'], ['$wrongOperator' => ['status' => 1]]); + } + /** * @depends testBatchInsert */ @@ -240,54 +294,54 @@ class CollectionTest extends MongoDbTestCase $this->assertEquals($expectedRows, $rows); } - /** - * @depends testMapReduce - */ - public function testMapReduceInline() - { - $collection = $this->getConnection()->getCollection('customer'); - $rows = [ - [ - 'name' => 'customer 1', - 'status' => 1, - 'amount' => 100, - ], - [ - 'name' => 'customer 2', - 'status' => 1, - 'amount' => 200, - ], - [ - 'name' => 'customer 2', - 'status' => 2, - 'amount' => 400, - ], - [ - 'name' => 'customer 2', - 'status' => 3, - 'amount' => 500, - ], - ]; - $collection->batchInsert($rows); - - $result = $collection->mapReduce( - 'function () {emit(this.status, this.amount)}', - 'function (key, values) {return Array.sum(values)}', - ['inline' => true], - ['status' => ['$lt' => 3]] - ); - $expectedRows = [ - [ - '_id' => 1, - 'value' => 300, - ], - [ - '_id' => 2, - 'value' => 400, - ], - ]; - $this->assertEquals($expectedRows, $result); - } + /** + * @depends testMapReduce + */ + public function testMapReduceInline() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = [ + [ + 'name' => 'customer 1', + 'status' => 1, + 'amount' => 100, + ], + [ + 'name' => 'customer 2', + 'status' => 1, + 'amount' => 200, + ], + [ + 'name' => 'customer 2', + 'status' => 2, + 'amount' => 400, + ], + [ + 'name' => 'customer 2', + 'status' => 3, + 'amount' => 500, + ], + ]; + $collection->batchInsert($rows); + + $result = $collection->mapReduce( + 'function () {emit(this.status, this.amount)}', + 'function (key, values) {return Array.sum(values)}', + ['inline' => true], + ['status' => ['$lt' => 3]] + ); + $expectedRows = [ + [ + '_id' => 1, + 'value' => 300, + ], + [ + '_id' => 2, + 'value' => 400, + ], + ]; + $this->assertEquals($expectedRows, $result); + } public function testCreateIndex() { diff --git a/tests/unit/extensions/redis/ActiveRecordTest.php b/tests/unit/extensions/redis/ActiveRecordTest.php index 5a2102f..a66ef87 100644 --- a/tests/unit/extensions/redis/ActiveRecordTest.php +++ b/tests/unit/extensions/redis/ActiveRecordTest.php @@ -33,13 +33,13 @@ class ActiveRecordTest extends RedisTestCase ActiveRecord::$db = $this->getConnection(); $customer = new Customer(); - $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1], false); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); $customer->save(false); $customer = new Customer(); - $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1], false); + $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1, 'profile_id' => null], false); $customer->save(false); $customer = new Customer(); - $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2], false); + $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2, 'profile_id' => 2], false); $customer->save(false); // INSERT INTO tbl_category (name) VALUES ('Books'); diff --git a/tests/unit/framework/ar/ActiveRecordTestTrait.php b/tests/unit/framework/ar/ActiveRecordTestTrait.php index 6bc27d4..cde8402 100644 --- a/tests/unit/framework/ar/ActiveRecordTestTrait.php +++ b/tests/unit/framework/ar/ActiveRecordTestTrait.php @@ -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); } diff --git a/tests/unit/framework/base/ExceptionTest.php b/tests/unit/framework/base/ExceptionTest.php index 635b55c..136fc28 100644 --- a/tests/unit/framework/base/ExceptionTest.php +++ b/tests/unit/framework/base/ExceptionTest.php @@ -15,7 +15,7 @@ class ExceptionTest extends TestCase $this->assertEquals('bar', $array['message']); $this->assertEquals('foo', $array['previous']['message']); - $e = new InvalidCallException('bar', 0 ,new UserException('foo')); + $e = new InvalidCallException('bar', 0, new UserException('foo')); $array = $e->toArray(); $this->assertEquals('bar', $array['message']); $this->assertEquals('foo', $array['previous']['message']); diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index d304741..88926a9 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -147,7 +147,7 @@ class ModelTest extends TestCase $this->assertTrue($speaker->hasErrors('firstName')); $this->assertFalse($speaker->hasErrors('lastName')); - $this->assertEquals(['Something is wrong!'], $speaker->getFirstErrors()); + $this->assertEquals(['firstName' => 'Something is wrong!'], $speaker->getFirstErrors()); $this->assertEquals('Something is wrong!', $speaker->getFirstError('firstName')); $this->assertNull($speaker->getFirstError('lastName')); diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 26515df..6ced6c7 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -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; /** @@ -257,6 +258,16 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertTrue($orders[0]->isRelationPopulated('customer')); $this->assertTrue($orders[1]->isRelationPopulated('customer')); + // inner join filtering, eager loading, conditions on both primary and relation + $orders = Order::find()->innerJoinWith([ + 'customer' => function ($query) { + $query->where(['tbl_customer.id' => 2]); + }, + ])->where(['tbl_order.id' => [1, 2]])->orderBy('tbl_order.id')->all(); + $this->assertEquals(1, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + // inner join filtering without eager loading $orders = Order::find()->innerJoinWith([ 'customer' => function ($query) { @@ -269,6 +280,16 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertFalse($orders[0]->isRelationPopulated('customer')); $this->assertFalse($orders[1]->isRelationPopulated('customer')); + // inner join filtering without eager loading, conditions on both primary and relation + $orders = Order::find()->innerJoinWith([ + 'customer' => function ($query) { + $query->where(['tbl_customer.id' => 2]); + }, + ], false)->where(['tbl_order.id' => [1, 2]])->orderBy('tbl_order.id')->all(); + $this->assertEquals(1, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertFalse($orders[0]->isRelationPopulated('customer')); + // join with via-relation $orders = Order::find()->innerJoinWith('books')->orderBy('tbl_order.id')->all(); $this->assertEquals(2, count($orders)); @@ -281,6 +302,9 @@ class ActiveRecordTest extends DatabaseTestCase // join with sub-relation $orders = Order::find()->innerJoinWith([ + 'items' => function ($q) { + $q->orderBy('tbl_item.id'); + }, 'items.category' => function ($q) { $q->where('tbl_category.id = 2'); }, @@ -341,6 +365,38 @@ 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' => function ($q) { + $q->orderBy('tbl_order.id'); + } + ])->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 diff --git a/tests/unit/framework/rbac/ManagerTestCase.php b/tests/unit/framework/rbac/ManagerTestCase.php index 6d80287..51c195e 100644 --- a/tests/unit/framework/rbac/ManagerTestCase.php +++ b/tests/unit/framework/rbac/ManagerTestCase.php @@ -173,7 +173,7 @@ abstract class ManagerTestCase extends TestCase $this->assertTrue($this->auth->executeBizRule(null, [], null)); $this->assertTrue($this->auth->executeBizRule('return 1 == true;', [], null)); $this->assertTrue($this->auth->executeBizRule('return $params[0] == $params[1];', [1, '1'], null)); - if (defined('HHVM_VERSION')) { // invalid code crashes on HHVM + if (!defined('HHVM_VERSION')) { // invalid code crashes on HHVM $this->assertFalse($this->auth->executeBizRule('invalid;', [], null)); } } diff --git a/tests/unit/framework/web/RequestTest.php b/tests/unit/framework/web/RequestTest.php new file mode 100644 index 0000000..656a517 --- /dev/null +++ b/tests/unit/framework/web/RequestTest.php @@ -0,0 +1,41 @@ +<?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')); + } +} diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index 39fa9bd..2909e35 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -12,6 +12,12 @@ use yiiunit\TestCase; */ class UrlRuleTest extends TestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + public function testCreateUrl() { $manager = new UrlManager(['cache' => null]);