diff --git a/app/assets/.gitignore b/app/assets/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/app/assets/.gitignore @@ -0,0 +1 @@ +* diff --git a/app/index.php b/app/index.php new file mode 100644 index 0000000..8f98090 --- /dev/null +++ b/app/index.php @@ -0,0 +1,9 @@ +run(); diff --git a/app/protected/config/main.php b/app/protected/config/main.php new file mode 100644 index 0000000..e18ead8 --- /dev/null +++ b/app/protected/config/main.php @@ -0,0 +1,15 @@ + 'hello', + 'basePath' => dirname(__DIR__), + 'components' => array( + 'cache' => array( + 'class' => 'yii\caching\FileCache', + ), + 'user' => array( + 'class' => 'yii\web\User', + 'identityClass' => 'app\models\User', + ) + ), +); \ No newline at end of file diff --git a/app/protected/controllers/SiteController.php b/app/protected/controllers/SiteController.php new file mode 100644 index 0000000..58e9568 --- /dev/null +++ b/app/protected/controllers/SiteController.php @@ -0,0 +1,22 @@ +render('index'); + } + + public function actionLogin() + { + $user = app\models\User::findIdentity(100); + Yii::$app->getUser()->login($user); + Yii::$app->getResponse()->redirect(array('site/index')); + } + + public function actionLogout() + { + Yii::$app->getUser()->logout(); + Yii::$app->getResponse()->redirect(array('site/index')); + } +} \ No newline at end of file diff --git a/app/protected/models/User.php b/app/protected/models/User.php new file mode 100644 index 0000000..cebf1da --- /dev/null +++ b/app/protected/models/User.php @@ -0,0 +1,43 @@ + array( + 'id' => '100', + 'authKey' => 'test100key', + 'name' => 'admin', + ), + '101' => array( + 'id' => '101', + 'authKey' => 'test101key', + 'name' => 'demo', + ), + ); + + public static function findIdentity($id) + { + return isset(self::$users[$id]) ? new self(self::$users[$id]) : null; + } + + public function getId() + { + return $this->id; + } + + public function getAuthKey() + { + return $this->authKey; + } + + public function validateAuthKey($authKey) + { + return $this->authKey === $authKey; + } +} \ No newline at end of file diff --git a/app/protected/runtime/.gitignore b/app/protected/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/app/protected/runtime/.gitignore @@ -0,0 +1 @@ +* diff --git a/app/protected/views/layouts/main.php b/app/protected/views/layouts/main.php new file mode 100644 index 0000000..092e665 --- /dev/null +++ b/app/protected/views/layouts/main.php @@ -0,0 +1,22 @@ + + + +beginPage(); ?> + + <?php echo Html::encode($this->title); ?> + head(); ?> + + +

Welcome

+beginBody(); ?> + +endBody(); ?> + +endPage(); ?> + diff --git a/app/protected/views/site/index.php b/app/protected/views/site/index.php new file mode 100644 index 0000000..3b83080 --- /dev/null +++ b/app/protected/views/site/index.php @@ -0,0 +1,17 @@ +title = 'Hello World'; + +$user = Yii::$app->getUser(); +if ($user->isGuest) { + echo Html::a('login', array('login')); +} else { + echo "You are logged in as " . $user->identity->name . "
"; + echo Html::a('logout', array('logout')); +} +?> + + diff --git a/framework/base/Controller.php b/framework/base/Controller.php index d11d19b..0d46235 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -19,7 +19,14 @@ use yii\helpers\StringHelper; */ class Controller extends Component { + /** + * @event ActionEvent an event raised right before executing a controller action. + * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. + */ const EVENT_BEFORE_ACTION = 'beforeAction'; + /** + * @event ActionEvent an event raised right after executing a controller action. + */ const EVENT_AFTER_ACTION = 'afterAction'; /** @@ -105,16 +112,15 @@ class Controller extends Component if ($action !== null) { $oldAction = $this->action; $this->action = $action; - - if ($this->beforeAction($action)) { - $status = $action->runWithParams($params); - $this->afterAction($action); - } else { - $status = 1; + $status = 1; + if ($this->module->beforeAction($action)) { + if ($this->beforeAction($action)) { + $status = $action->runWithParams($params); + $this->afterAction($action); + } + $this->module->afterAction($action); } - $this->action = $oldAction; - return $status; } else { throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id); diff --git a/framework/base/Module.php b/framework/base/Module.php index d99778d..959bb04 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -9,7 +9,6 @@ namespace yii\base; use Yii; use yii\helpers\StringHelper; -use yii\helpers\FileHelper; /** * Module is the base class for module and application classes. @@ -39,6 +38,15 @@ use yii\helpers\FileHelper; abstract class Module extends Component { /** + * @event ActionEvent an event raised before executing a controller action. + * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. + */ + const EVENT_BEFORE_ACTION = 'beforeAction'; + /** + * @event ActionEvent an event raised after executing a controller action. + */ + const EVENT_AFTER_ACTION = 'afterAction'; + /** * @var array custom module parameters (name => value). */ public $params = array(); @@ -613,4 +621,27 @@ abstract class Module extends Component return isset($controller) ? array($controller, $route) : false; } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $event = new ActionEvent($action); + $this->trigger(self::EVENT_BEFORE_ACTION, $event); + return $event->isValid; + } + + /** + * 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. + */ + public function afterAction($action) + { + $this->trigger(self::EVENT_AFTER_ACTION, new ActionEvent($action)); + } } diff --git a/framework/caching/ApcCache.php b/framework/caching/ApcCache.php index dd954cc..391851d 100644 --- a/framework/caching/ApcCache.php +++ b/framework/caching/ApcCache.php @@ -11,6 +11,7 @@ namespace yii\caching; * ApcCache provides APC caching in terms of an application component. * * To use this application component, the [APC PHP extension](http://www.php.net/apc) must be loaded. + * In order to enable APC for CLI you should add "apc.enable_cli = 1" to your php.ini. * * See [[Cache]] for common cache operations that ApcCache supports. * diff --git a/framework/console/Application.php b/framework/console/Application.php index e185cc5..2f28cac 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -129,7 +129,7 @@ class Application extends \yii\base\Application 'migrate' => 'yii\console\controllers\MigrateController', 'app' => 'yii\console\controllers\AppController', 'cache' => 'yii\console\controllers\CacheController', - 'script' => 'yii\console\controllers\ScriptController', + 'asset' => 'yii\console\controllers\AssetController', ); } diff --git a/framework/console/controllers/ScriptController.php b/framework/console/controllers/AssetController.php similarity index 67% rename from framework/console/controllers/ScriptController.php rename to framework/console/controllers/AssetController.php index ab57f92..71a2cae 100644 --- a/framework/console/controllers/ScriptController.php +++ b/framework/console/controllers/AssetController.php @@ -15,7 +15,7 @@ use yii\console\Controller; * @author Qiang Xue * @since 2.0 */ -class ScriptController extends Controller +class AssetController extends Controller { public $defaultAction = 'compress'; @@ -33,13 +33,15 @@ class ScriptController extends Controller */ public $targets = array(); public $assetManager = array(); + public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}'; + public $cssCompressor = 'java -jar yuicompressor.jar {from} -o {to}'; public function actionCompress($configFile, $bundleFile) { $this->loadConfiguration($configFile); $bundles = $this->loadBundles($this->bundles, $this->extensions); $targets = $this->loadTargets($this->targets, $bundles); -// $this->publishBundles($bundles, $this->publishOptions); + $this->publishBundles($bundles, $this->publishOptions); $timestamp = time(); foreach ($targets as $target) { if (!empty($target->js)) { @@ -96,11 +98,38 @@ class ScriptController extends Controller protected function loadTargets($targets, $bundles) { + // build the dependency order of bundles $registered = array(); foreach ($bundles as $name => $bundle) { $this->registerBundle($bundles, $name, $registered); } $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1)); + + // fill up the target which has empty 'depends'. + $referenced = array(); + foreach ($targets as $name => $target) { + if (empty($target['depends'])) { + if (!isset($all)) { + $all = $name; + } else { + throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name"); + } + } else { + foreach ($target['depends'] as $bundle) { + if (!isset($referenced[$bundle])) { + $referenced[$bundle] = $name; + } else { + throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time."); + } + } + } + } + if (isset($all)) { + $targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced)); + } + + // adjust the 'depends' order for each target according to the dependency order of bundles + // create an AssetBundle object for each target foreach ($targets as $name => $target) { if (!isset($target['basePath'])) { throw new Exception("Please specify 'basePath' for the '$name' target."); @@ -172,11 +201,7 @@ class ScriptController extends Controller $map = array(); foreach ($targets as $name => $target) { foreach ($target->depends as $bundle) { - if (!isset($map[$bundle])) { - $map[$bundle] = $name; - } else { - throw new Exception("Bundle '$bundle' is found in both target '{$map[$bundle]}' and '$name'."); - } + $map[$bundle] = $name; } } @@ -236,7 +261,8 @@ class ScriptController extends Controller file_put_contents($bundleFile, <<jsCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineJsFiles($inputFiles, $tmpFile); + $log = shell_exec(strtr($this->jsCompressor, array( + '{from}' => $tmpFile, + '{to}' => $outputFile, + ))); + @unlink($tmpFile); + } else { + $log = call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile); + } } protected function compressCssFiles($inputFiles, $outputFile) { + if (is_string($this->cssCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineCssFiles($inputFiles, $tmpFile); + $log = shell_exec(strtr($this->cssCompressor, array( + '{from}' => $inputFiles, + '{to}' => $outputFile, + ))); + } else { + $log = call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile); + } + } + + public function combineJsFiles($files, $tmpFile) + { + $content = ''; + foreach ($files as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + file_put_contents($tmpFile, $content); + } + + public function combineCssFiles($files, $tmpFile) + { + // todo: adjust url() references in CSS files + $content = ''; + foreach ($files as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + file_put_contents($tmpFile, $content); + } + + public function actionTemplate($configFile) + { + $template = << require('path/to/bundles.php'), + // + 'extensions' => require('path/to/namespaces.php'), + // + 'targets' => array( + 'all' => array( + 'basePath' => __DIR__, + 'baseUrl' => '/test', + 'js' => 'all-{ts}.js', + 'css' => 'all-{ts}.css', + ), + ), + 'assetManager' => array( + 'basePath' => __DIR__, + 'baseUrl' => '/test', + ), +); +EOD; + file_put_contents($configFile, $template); } } \ No newline at end of file diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 695034a..797508a 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -522,7 +522,7 @@ class Connection extends Component if (isset($matches[3])) { return $db->quoteColumnName($matches[3]); } else { - return str_replace('%', $this->tablePrefix, $db->quoteTableName($matches[2])); + return str_replace('%', $db->tablePrefix, $db->quoteTableName($matches[2])); } }, $sql); } diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index da43940..441d287 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -744,7 +744,7 @@ class QueryBuilder extends \yii\base\Object $parts = array(); foreach ($condition as $column => $value) { if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition('in', array($column, $value), $query); + $parts[] = $this->buildInCondition('in', array($column, $value), $params); } else { if (strpos($column, '(') === false) { $column = $this->db->quoteColumnName($column); diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 459e8e8..755d644 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -74,9 +74,6 @@ class UrlManager extends Component public function init() { parent::init(); - if (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } $this->compileRules(); } @@ -88,6 +85,9 @@ class UrlManager extends Component if (!$this->enablePrettyUrl || $this->rules === array()) { return; } + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } if ($this->cache instanceof Cache) { $key = $this->cache->buildKey(__CLASS__); $hash = md5(json_encode($this->rules)); @@ -104,7 +104,7 @@ class UrlManager extends Component $this->rules[$i] = Yii::createObject($rule); } - if ($this->cache instanceof Cache) { + if (isset($key, $hash)) { $this->cache->set($key, array($this->rules, $hash)); } } diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 4a388c6..8290bbe 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -9,4 +9,6 @@ require_once(__DIR__ . '/../../framework/yii.php'); Yii::setAlias('@yiiunit', __DIR__); +new \yii\web\Application(array('id' => 'testapp', 'basePath' => __DIR__)); + require_once(__DIR__ . '/TestCase.php'); diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index aa15230..f04e550 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -195,7 +195,7 @@ class ModelTest extends TestCase public function testCreateValidators() { - $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must be an array specifying both attribute names and validator type.'); + $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must specify both attribute names and validator type.'); $invalid = new InvalidRulesModel(); $invalid->createValidators(); diff --git a/tests/unit/framework/caching/CacheTest.php b/tests/unit/framework/caching/CacheTest.php index ad2fcf5..f9db4f4 100644 --- a/tests/unit/framework/caching/CacheTest.php +++ b/tests/unit/framework/caching/CacheTest.php @@ -16,9 +16,9 @@ abstract class CacheTest extends TestCase public function testSet() { $cache = $this->getCacheInstance(); - $cache->set('string_test', 'string_test'); - $cache->set('number_test', 42); - $cache->set('array_test', array('array_test' => 'array_test')); + $this->assertTrue($cache->set('string_test', 'string_test')); + $this->assertTrue($cache->set('number_test', 42)); + $this->assertTrue($cache->set('array_test', array('array_test' => 'array_test'))); $cache['arrayaccess_test'] = new \stdClass(); } @@ -45,7 +45,7 @@ abstract class CacheTest extends TestCase public function testExpire() { $cache = $this->getCacheInstance(); - $cache->set('expire_test', 'expire_test', 2); + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); sleep(1); $this->assertEquals('expire_test', $cache->get('expire_test')); sleep(2); @@ -57,11 +57,11 @@ abstract class CacheTest extends TestCase $cache = $this->getCacheInstance(); // should not change existing keys - $cache->add('number_test', 13); + $this->assertFalse($cache->add('number_test', 13)); $this->assertEquals(42, $cache->get('number_test')); // should store data is it's not there yet - $cache->add('add_test', 13); + $this->assertTrue($cache->add('add_test', 13)); $this->assertEquals(13, $cache->get('add_test')); } @@ -69,14 +69,14 @@ abstract class CacheTest extends TestCase { $cache = $this->getCacheInstance(); - $cache->delete('number_test'); + $this->assertTrue($cache->delete('number_test')); $this->assertEquals(null, $cache->get('number_test')); } public function testFlush() { $cache = $this->getCacheInstance(); - $cache->flush(); + $this->assertTrue($cache->flush()); $this->assertEquals(null, $cache->get('add_test')); } } diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index bf0ca0a..4077043 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -22,6 +22,14 @@ class HtmlTest extends \yii\test\TestCase )); } + public function assertEqualsWithoutLE($expected, $actual) + { + $expected = str_replace("\r\n", "\n", $expected); + $actual = str_replace("\r\n", "\n", $actual); + + $this->assertEquals($expected, $actual); + } + public function tearDown() { Yii::$app = null; @@ -240,21 +248,21 @@ class HtmlTest extends \yii\test\TestCase EOD; - $this->assertEquals($expected, Html::dropDownList('test')); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test')); $expected = << EOD; - $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', null, $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); } public function testListBox() @@ -264,48 +272,48 @@ EOD; EOD; - $this->assertEquals($expected, Html::listBox('test')); + $this->assertEqualsWithoutLE($expected, Html::listBox('test')); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems2())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', 'value2', $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, array(), array('multiple' => true))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); } public function testCheckboxList() @@ -316,19 +324,19 @@ EOD; EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); $expected = <<
EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
\n", 'unselect' => '0', ))); @@ -337,7 +345,7 @@ EOD; 0 1 EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'item' => function ($index, $label, $name, $checked, $value) { return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); } @@ -352,19 +360,19 @@ EOD; EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); $expected = <<
EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
\n", 'unselect' => '0', ))); @@ -373,7 +381,7 @@ EOD; 0 1 EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'item' => function ($index, $label, $name, $checked, $value) { return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); } @@ -420,7 +428,7 @@ EOD; 'group12' => array('class' => 'group'), ), ); - $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); } public function testRenderAttributes()