From 9f062332752be307dea044ca6640e71c1f49de27 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 25 Sep 2012 13:36:50 +0200 Subject: [PATCH 1/4] first draft of ANSI control sequence implementation Moving cursors, clear page and line, hide/show cursor implementation is mostly complete. Need to rework color api now. sources: http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes https://github.com/pear/Console_Color2/blob/master/Console/Color2.php --- framework/console/Controller.php | 345 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 042a084..18180e5 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -24,10 +24,42 @@ use yii\base\Exception; * ~~~ * * @author Qiang Xue + * @author Carsten Brandt * @since 2.0 */ class Controller extends \yii\base\Controller { + const FG_COLOR_BLACK = 30; + const FG_COLOR_RED = 31; + const FG_COLOR_GREEN = 32; + const FG_COLOR_YELLOW = 33; + const FG_COLOR_BLUE = 34; + const FG_COLOR_PURPLE = 35; + const FG_COLOR_CYAN = 36; + const FG_COLOR_GREY = 37; + + const BG_COLOR_BLACK = 40; + const BG_COLOR_RED = 41; + const BG_COLOR_GREEN = 42; + const BG_COLOR_YELLOW = 43; + const BG_COLOR_BLUE = 44; + const BG_COLOR_PURPLE = 45; + const BG_COLOR_CYAN = 46; + const BG_COLOR_GREY = 47; + + const TEXT_BOLD = 1; + const TEXT_ITALIC = 3; + const TEXT_UNDERLINE = 4; + const TEXT_BLINK = 5; + const TEXT_NEGATIVE = 7; + const TEXT_CONCEALED = 8; + const TEXT_CROSSED_OUT = 9; + const TEXT_FRAMED = 51; + const TEXT_ENCIRCLED = 52; + const TEXT_OVERLINED = 53; + + public $color = null; + /** * This method is invoked when the request parameters do not satisfy the requirement of the specified action. * The default implementation will throw an exception. @@ -112,4 +144,317 @@ class Controller extends \yii\base\Controller $input = trim(fgets(STDIN)); return empty($input) ? $default : !strncasecmp($input, 'y', 1); } + + /** + * Moves the terminal cursor up by sending ANSI code CUU to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved up + */ + public function moveCursorUp($rows=1) + { + echo "\033[" . (int) $rows . 'A'; + } + + /** + * Moves the terminal cursor down by sending ANSI code CUD to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved down + */ + public function moveCursorDown($rows=1) + { + echo "\033[" . (int) $rows . 'B'; + } + + /** + * Moves the terminal cursor forward by sending ANSI code CUF to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved forward + */ + public function moveCursorForward($steps=1) + { + echo "\033[" . (int) $steps . 'C'; + } + + /** + * Moves the terminal cursor backward by sending ANSI code CUB to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved backward + */ + public function moveCursorBackward($steps=1) + { + echo "\033[" . (int) $steps . 'D'; + } + + /** + * Moves the terminal cursor to the beginning of the next line by sending ANSI code CNL to the terminal. + * @param integer $lines number of lines the cursor should be moved down + */ + public function moveCursorNextLine($lines=1) + { + echo "\033[" . (int) $lines . 'E'; + } + + /** + * Moves the terminal cursor to the beginning of the previous line by sending ANSI code CPL to the terminal. + * @param integer $lines number of lines the cursor should be moved up + */ + public function moveCursorPrevLine($lines=1) + { + echo "\033[" . (int) $lines . 'F'; + } + + /** + * Moves the cursor to an absolute position given as column and row by sending ANSI code CUP or CHA to the terminal. + * + * @param integer $column 1-based column number, 1 is the left edge of the screen. + * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. + */ + public function moveCursorTo($column, $row=null) + { + if ($row === null) { + echo "\033[" . (int) $column . 'G'; + } else { + echo "\033[" . (int) $row . ';' . (int) $column . 'H'; + } + } + + /** + * Scrolls whole page up by sending ANSI code SU to the terminal. + * New lines are added at the bottom. This is not supported by ANSI.SYS + * @param int $lines number of lines to scroll up + */ + public function scrollUp($lines=1) + { + echo "\033[".(int)$lines."S"; + } + + /** + * Scrolls whole page down by sending ANSI code SD to the terminal. + * New lines are added at the top. This is not supported by ANSI.SYS + * @param int $lines number of lines to scroll down + */ + public function scrollDown($lines=1) + { + echo "\033[".(int)$lines."T"; + } + + /** + * Saves the current cursor position by sending ANSI code SCP to the terminal. + * Position can then be restored with {@link restoreCursorPosition} + */ + public function saveCursorPosition() + { + echo "\033[s"; + } + + /** + * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI code RCP to the terminal. + */ + public function restoreCursorPosition() + { + echo "\033[u"; + } + + /** + * Hides the cursor by sending ANSI by sending ANSI DECTCEM code ?25l to the terminal. + * Use {@link showCursor} to bring it back. + */ + public function hideCursor() + { + echo "\033[?25l"; + } + + /** + * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. + */ + public function showCursor() + { + echo "\033[?25h"; + } + + /** + * clears entire screen content by sending ANSI code ED with argument 2 to the terminal + * Cursor position will not be changed (ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen). + */ + public function clearScreen() + { + echo "\033[2J"; + } + + /** + * clears text from cursor to the beginning of the screen by sending ANSI code ED with argument 1 to the terminal + * Cursor position will not be changed. + */ + public function clearScreenBeforeCursor() + { + echo "\033[1J"; + } + + /** + * clears text from cursor to the end of the screen by sending ANSI code ED with argument 0 to the terminal + * Cursor position will not be changed. + */ + public function clearScreenAfterCursor() + { + echo "\033[0J"; + } + + + /** + * clears entire screen content by sending ANSI code EL with argument 2 to the terminal + * Cursor position will not be changed. + */ + public function clearLine() + { + echo "\033[2K"; + } + + /** + * clears text from cursor to the beginning of the screen by sending ANSI code EL with argument 1 to the terminal + * Cursor position will not be changed. + */ + public function clearLineBeforeCursor() + { + echo "\033[1K"; + } + + /** + * clears text from cursor to the end of the screen by sending ANSI code EL with argument 0 to the terminal + * Cursor position will not be changed. + */ + public function clearLineAfterCursor() + { + echo "\033[0K"; + } + + + + + + + //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors + public function xtermColor($i) { + + } + + + + + + + + /** + * This method will turn given string into one colorized with ansi color + */ + public function colorize($string, $foreground = null, $background = null, $style = null) + { + $codes = array(); + if ($foreground !== null) { + $codes[] = static::FOREGROUND_COLOR + $foreground; + } + if ($background !== null) { + $codes[] = static::BACKGROUND_COLOR + $background; + } + if ($style !== null) { + $codes[] = $style; + } + + $code = implode(';', $codes); + return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string . "\033[0m"; + } + + + public function style($code) + { + return "\033[{$code}m"; + } + + public function color($foreground, $background=null) + { + if ($foreground === null && $background === null) { + return ''; + } + + $codes = array(); + if ($foreground !== null) { + $codes[] = static::FOREGROUND_COLOR + $foreground; + } + if ($background !== null) { + $codes[] = static::BACKGROUND_COLOR + $background; + } + + return "\033[" . implode(';', $codes) . "m"; + } + + public function reset() + { + return "\033[0m"; + } + + + public function renderColoredString($string) + { + + } + + + // TODO refactor and review + public function ansiToHtml($string) + { + $tags = 0; + return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { + $styleA = array(); + foreach(explode(';', $ansi) as $controlCode) + { + switch($controlCode) + { + case static::FG_COLOR_BLACK: $style = array('color' => '#000000'); break; + case static::FG_COLOR_BLUE: $style = array('color' => '#000078'); break; + case static::FG_COLOR_CYAN: $style = array('color' => '#007878'); break; + case static::FG_COLOR_GREEN: $style = array('color' => '#007800'); break; + case static::FG_COLOR_GREY: $style = array('color' => '#787878'); break; + case static::FG_COLOR_PURPLE: $style = array('color' => '#780078'); break; + case static::FG_COLOR_RED: $style = array('color' => '#780000'); break; + case static::FG_COLOR_YELLOW: $style = array('color' => '#787800'); break; + case static::BG_COLOR_BLACK: $style = array('background-color' => '#000000'); break; + case static::BG_COLOR_BLUE: $style = array('background-color' => '#000078'); break; + case static::BG_COLOR_CYAN: $style = array('background-color' => '#007878'); break; + case static::BG_COLOR_GREEN: $style = array('background-color' => '#007800'); break; + case static::BG_COLOR_GREY: $style = array('background-color' => '#787878'); break; + case static::BG_COLOR_PURPLE: $style = array('background-color' => '#780078'); break; + case static::BG_COLOR_RED: $style = array('background-color' => '#780000'); break; + case static::BG_COLOR_YELLOW: $style = array('background-color' => '#787800'); break; + case static::TEXT_BOLD: $style = array('font-weight' => 'bold'); break; + case static::TEXT_ITALIC: $style = array('font-style' => 'italic'); break; + case static::TEXT_UNDERLINE: $style = array('text-decoration' => array('underline')); break; + case static::TEXT_OVERLINED: $style = array('text-decoration' => array('overline')); break; + case static::TEXT_CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; + case static::TEXT_BLINK: $style = array('text-decoration' => array('blink')); break; + case static::TEXT_NEGATIVE: // ??? + case static::TEXT_CONCEALED: + case static::TEXT_ENCIRCLED: + case static::TEXT_FRAMED: + // TODO allow resetting codes + break; + case 0: // ansi reset + $return = ''; + for($n=$tags; $tags>0; $tags--) { + $return .= ''; + } + return $return; + } + $styleA = \yii\util\ArrayHelper::merge($styleA, $style); + } + $styleString[] = array(); + foreach($styleA as $name => $content) { + if ($name = 'text-decoration') { + $content = implode(' ', $content); + } + $styleString[] = $name.':'.$content; + } + $tags++; + return 'getScreenSize(); + * + * @return array + */ + public function getScreenSize() + { + // TODO implement + return array(150,50); } + /** + * resets any ansi style set by previous method {@link ansiStyle} + * Any output after this is will have default text style. + */ public function reset() { - return "\033[0m"; + echo "\033[0m"; } - - public function renderColoredString($string) + /** + * Strips ANSI control codes from a string + * + * @param string $string String to strip + * @return string + */ + function strip($string) { - + return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color } - // TODO refactor and review public function ansiToHtml($string) { @@ -457,4 +455,115 @@ class Controller extends \yii\base\Controller }, $string); } + /** + * TODO syntax copied from https://github.com/pear/Console_Color2/blob/master/Console/Color2.php + * + * Converts colorcodes in the format %y (for yellow) into ansi-control + * codes. The conversion table is: ('bold' meaning 'light' on some + * terminals). It's almost the same conversion table irssi uses. + *
+	 *                  text      text            background
+	 *      ------------------------------------------------
+	 *      %k %K %0    black     dark grey       black
+	 *      %r %R %1    red       bold red        red
+	 *      %g %G %2    green     bold green      green
+	 *      %y %Y %3    yellow    bold yellow     yellow
+	 *      %b %B %4    blue      bold blue       blue
+	 *      %m %M %5    magenta   bold magenta    magenta
+	 *      %p %P       magenta (think: purple)
+	 *      %c %C %6    cyan      bold cyan       cyan
+	 *      %w %W %7    white     bold white      white
+	 *
+	 *      %F     Blinking, Flashing
+	 *      %U     Underline
+	 *      %8     Reverse
+	 *      %_,%9  Bold
+	 *
+	 *      %n     Resets the color
+	 *      %%     A single %
+	 * 
+ * First param is the string to convert, second is an optional flag if + * colors should be used. It defaults to true, if set to false, the + * colorcodes will just be removed (And %% will be transformed into %) + * + * @param string $string String to convert + * @param bool $colored Should the string be colored? + * + * @access public + * @return string + */ + public function renderColoredString($string) + { + $colored = true; + + + static $conversions = array ( // static so the array doesn't get built + // everytime + // %y - yellow, and so on... {{{ + '%y' => array('color' => 'yellow'), + '%g' => array('color' => 'green' ), + '%b' => array('color' => 'blue' ), + '%r' => array('color' => 'red' ), + '%p' => array('color' => 'purple'), + '%m' => array('color' => 'purple'), + '%c' => array('color' => 'cyan' ), + '%w' => array('color' => 'grey' ), + '%k' => array('color' => 'black' ), + '%n' => array('color' => 'reset' ), + '%Y' => array('color' => 'yellow', 'style' => 'light'), + '%G' => array('color' => 'green', 'style' => 'light'), + '%B' => array('color' => 'blue', 'style' => 'light'), + '%R' => array('color' => 'red', 'style' => 'light'), + '%P' => array('color' => 'purple', 'style' => 'light'), + '%M' => array('color' => 'purple', 'style' => 'light'), + '%C' => array('color' => 'cyan', 'style' => 'light'), + '%W' => array('color' => 'grey', 'style' => 'light'), + '%K' => array('color' => 'black', 'style' => 'light'), + '%N' => array('color' => 'reset', 'style' => 'light'), + '%3' => array('background' => 'yellow'), + '%2' => array('background' => 'green' ), + '%4' => array('background' => 'blue' ), + '%1' => array('background' => 'red' ), + '%5' => array('background' => 'purple'), + '%6' => array('background' => 'cyan' ), + '%7' => array('background' => 'grey' ), + '%0' => array('background' => 'black' ), + // Don't use this, I can't stand flashing text + '%F' => array('style' => 'blink'), + '%U' => array('style' => 'underline'), + '%8' => array('style' => 'inverse'), + '%9' => array('style' => 'bold'), + '%_' => array('style' => 'bold') + // }}} + ); + + if ($colored) { + $string = str_replace('%%', '% ', $string); + foreach ($conversions as $key => $value) { + $string = str_replace($key, Console_Color::color($value), + $string); + } + $string = str_replace('% ', '%', $string); + + } else { + $string = preg_replace('/%((%)|.)/', '$2', $string); + } + + return $string; + } + + /** + * Escapes % so they don't get interpreted as color codes + * + * @param string $string String to escape + * + * @access public + * @return string + */ + function escape($string) + { + return str_replace('%', '%%', $string); + } + + } \ No newline at end of file From a8be9ce412b3ce6291331886dbb7120ce21b9ce0 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 17 Nov 2012 08:35:53 -0500 Subject: [PATCH 3/4] new AR WIP --- framework/db/ar/ActiveQuery.php | 456 ++---------------------- framework/db/ar/ActiveRecord.php | 31 +- framework/db/ar/HasManyRelation.php | 8 + framework/db/ar/HasOneRelation.php | 8 + framework/db/ar/ManyManyRelation.php | 10 + framework/db/ar/Relation.php | 14 + framework/db/dao/BaseQuery.php | 100 +----- tests/unit/framework/db/ar/ActiveRecordTest.php | 88 +++-- 8 files changed, 167 insertions(+), 548 deletions(-) create mode 100644 framework/db/ar/HasManyRelation.php create mode 100644 framework/db/ar/HasOneRelation.php create mode 100644 framework/db/ar/ManyManyRelation.php create mode 100644 framework/db/ar/Relation.php diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index 01c7037..fbec6c9 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -15,7 +15,7 @@ use yii\base\VectorIterator; use yii\db\dao\Expression; use yii\db\Exception; -class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, \Countable +class ActiveQuery extends BaseQuery { /** * @var string the name of the ActiveRecord class. @@ -24,7 +24,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, /** * @var array list of relations that this query should be performed with */ - public $with; + public $with = array(); /** * @var string the name of the column that the result should be indexed by. * This is only useful when the query result is returned as an array. @@ -38,28 +38,12 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, /** * @var array list of scopes that should be applied to this query */ - public $scopes; + public $scopes = array(); /** * @var string the SQL statement to be executed for retrieving AR records. * This is set by [[ActiveRecord::findBySql()]]. */ public $sql; - /** - * @var array list of query results. Depending on [[asArray]], this can be either - * an array of AR objects (when [[asArray]] is false) or an array of array - * (when [[asArray]] is true). - */ - public $records; - - /** - * @param string $modelClass the name of the ActiveRecord class. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($modelClass, $config = array()) - { - $this->modelClass = $modelClass; - parent::__construct($config); - } public function __call($name, $params) { @@ -77,7 +61,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, */ public function all() { - return $this->findRecords(); + return $this->find(); } /** @@ -88,8 +72,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, */ public function one() { - $this->limit = 1; - $records = $this->findRecords(); + $records = $this->find(); return isset($records[0]) ? $records[0] : null; } @@ -125,15 +108,6 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, } /** - * Returns the number of items in the vector. - * @return integer the number of items in the vector - */ - public function getCount() - { - return $this->count(); - } - - /** * Sets the parameters about query caching. * This is a shortcut method to {@link CDbConnection::cache()}. * It changes the query caching parameter of the {@link dbConnection} instance. @@ -150,139 +124,45 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, return $this; } - /** - * Returns an iterator for traversing the items in the vector. - * This method is required by the SPL interface `IteratorAggregate`. - * It will be implicitly called when you use `foreach` to traverse the vector. - * @return VectorIterator an iterator for traversing the items in the vector. - */ - public function getIterator() - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return new VectorIterator($this->records); - } - - /** - * Returns the number of items in the vector. - * This method is required by the SPL `Countable` interface. - * It will be implicitly called when you use `count($vector)`. - * @return integer number of items in the vector. - */ - public function count() - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return count($this->records); - } - - /** - * Returns a value indicating whether there is an item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `isset($vector[$offset])`. - * @param integer $offset the offset to be checked - * @return boolean whether there is an item at the specified offset. - */ - public function offsetExists($offset) - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return isset($this->records[$offset]); - } - - /** - * Returns the item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$value = $vector[$offset];`. - * This is equivalent to [[itemAt]]. - * @param integer $offset the offset to retrieve item. - * @return ActiveRecord the item at the offset - * @throws Exception if the offset is out of range - */ - public function offsetGet($offset) - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return isset($this->records[$offset]) ? $this->records[$offset] : null; - } - - /** - * Sets the item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$vector[$offset] = $item;`. - * If the offset is null or equal to the number of the existing items, - * the new item will be appended to the vector. - * Otherwise, the existing item at the offset will be replaced with the new item. - * @param integer $offset the offset to set item - * @param ActiveRecord $item the item value - * @throws Exception if the offset is out of range, or the vector is read only. - */ - public function offsetSet($offset, $item) - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - $this->records[$offset] = $item; - } - - /** - * Unsets the item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `unset($vector[$offset])`. - * This is equivalent to [[removeAt]]. - * @param integer $offset the offset to unset item - * @throws Exception if the offset is out of range, or the vector is read only. - */ - public function offsetUnset($offset) - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - unset($this->records[$offset]); - } - - public function find() + protected function find() { + $modelClass = $this->modelClass; /** - * find the primary ARs - * for each child relation - * find the records filtered by the PK constraints - * populate primary ARs with the related records - * recursively call this metod again + * @var ActiveRecord $model */ - } - - protected function findByParent($parent) - { - - } - - protected function findRecords() - { - if (!empty($this->with)) { - return $this->findWithRelations(); - } - + $model = $modelClass::model(); + /** + * @var \yii\db\dao\Connection $db + */ + $db = $model->getDbConnection(); if ($this->sql === null) { if ($this->from === null) { - $modelClass = $this->modelClass; - $tableName = $modelClass::model()->getTableSchema()->name; + $tableName = $model->getTableSchema()->name; $this->from = array($tableName); } - $this->sql = $this->connection->getQueryBuilder()->build($this); + foreach ($this->scopes as $name => $config) { + if (is_integer($name)) { + $model->$config($this); + } else { + array_unshift($config, $this); + call_user_func_array(array($model, $name), $config); + } + } + $this->sql = $db->getQueryBuilder()->build($this); } - $command = $this->connection->createCommand($this->sql, $this->params); + $command = $db->createCommand($this->sql, $this->params); $rows = $command->queryAll(); - return $this->createRecords($rows); - } + $records = $this->createRecords($rows); - protected function findWithRelations() - { - $records = $this->findRecords(); + foreach ($this->with as $name => $config) { + $relation = $model->$name(); + foreach ($config as $p => $v) { + $relation->$p = $v; + } + $relation->findWith($records); + } + + return $records; } protected function createRecords($rows) @@ -311,271 +191,3 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, return $records; } } - - - -/** - * 1. eager loading, base limited and has has_many relations - * 2. - * ActiveFinder.php is ... - * - * @property integer $count - * - * @author Qiang Xue - * @since 2.0 - */ -class ActiveQuery2 extends BaseActiveQuery implements \IteratorAggregate, \ArrayAccess, \Countable -{ - /** - * @var string the SQL statement to be executed to retrieve primary records. - * This is set by [[ActiveRecord::findBySql()]]. - */ - public $sql; - /** - * @var array list of query results - */ - public $records; - - /** - * @param string $modelClass the name of the ActiveRecord class. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($modelClass, $config = array()) - { - $this->modelClass = $modelClass; - parent::__construct($config); - } - - public function __call($name, $params) - { - if (method_exists($this->modelClass, $name)) { - $this->scopes[$name] = $params; - return $this; - } else { - return parent::__call($name, $params); - } - } - - /** - * Executes query and returns all results as an array. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all() - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return $this->records; - } - - /** - * Executes query and returns a single row of result. - * @return null|array|ActiveRecord the single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. Null will be returned - * if the query results in nothing. - */ - public function one() - { - if ($this->records === null) { - $this->limit = 1; - $this->records = $this->findRecords(); - } - return isset($this->records[0]) ? $this->records[0] : null; - } - - /** - * Returns a scalar value for this query. - * The value returned will be the first column in the first row of the query results. - * @return string|boolean the value of the first column in the first row of the query result. - * False is returned if there is no value. - */ - public function value() - { - return $this->createFinder()->find($this, true); - } - - /** - * Executes query and returns if matching row exists in the table. - * @return bool if row exists in the table. - */ - public function exists() - { - return $this->select(array(new Expression('1')))->value() !== false; - } - - /** - * Returns the database connection used by this query. - * This method returns the connection used by the [[modelClass]]. - * @return \yii\db\dao\Connection the database connection used by this query - */ - public function getDbConnection() - { - $class = $this->modelClass; - return $class::getDbConnection(); - } - - /** - * Returns the number of items in the vector. - * @return integer the number of items in the vector - */ - public function getCount() - { - return $this->count(); - } - - /** - * Sets the parameters about query caching. - * This is a shortcut method to {@link CDbConnection::cache()}. - * It changes the query caching parameter of the {@link dbConnection} instance. - * @param integer $duration the number of seconds that query results may remain valid in cache. - * If this is 0, the caching will be disabled. - * @param \yii\caching\Dependency $dependency the dependency that will be used when saving the query results into cache. - * @param integer $queryCount number of SQL queries that need to be cached after calling this method. Defaults to 1, - * meaning that the next SQL query will be cached. - * @return ActiveRecord the active record instance itself. - */ - public function cache($duration, $dependency = null, $queryCount = 1) - { - $this->getDbConnection()->cache($duration, $dependency, $queryCount); - return $this; - } - - /** - * Returns an iterator for traversing the items in the vector. - * This method is required by the SPL interface `IteratorAggregate`. - * It will be implicitly called when you use `foreach` to traverse the vector. - * @return VectorIterator an iterator for traversing the items in the vector. - */ - public function getIterator() - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return new VectorIterator($this->records); - } - - /** - * Returns the number of items in the vector. - * This method is required by the SPL `Countable` interface. - * It will be implicitly called when you use `count($vector)`. - * @return integer number of items in the vector. - */ - public function count() - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return count($this->records); - } - - /** - * Returns a value indicating whether there is an item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `isset($vector[$offset])`. - * @param integer $offset the offset to be checked - * @return boolean whether there is an item at the specified offset. - */ - public function offsetExists($offset) - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return isset($this->records[$offset]); - } - - /** - * Returns the item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$value = $vector[$offset];`. - * This is equivalent to [[itemAt]]. - * @param integer $offset the offset to retrieve item. - * @return ActiveRecord the item at the offset - * @throws Exception if the offset is out of range - */ - public function offsetGet($offset) - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return isset($this->records[$offset]) ? $this->records[$offset] : null; - } - - /** - * Sets the item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$vector[$offset] = $item;`. - * If the offset is null or equal to the number of the existing items, - * the new item will be appended to the vector. - * Otherwise, the existing item at the offset will be replaced with the new item. - * @param integer $offset the offset to set item - * @param ActiveRecord $item the item value - * @throws Exception if the offset is out of range, or the vector is read only. - */ - public function offsetSet($offset, $item) - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - $this->records[$offset] = $item; - } - - /** - * Unsets the item at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `unset($vector[$offset])`. - * This is equivalent to [[removeAt]]. - * @param integer $offset the offset to unset item - * @throws Exception if the offset is out of range, or the vector is read only. - */ - public function offsetUnset($offset) - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - unset($this->records[$offset]); - } - - protected function findRecords() - { - return $this->createFinder()->find($this); - } - - protected function createFinder() - { - return new ActiveFinder($this->getDbConnection()); - } - - public function asArray($value = true) - { - $this->asArray = $value; - return $this; - } - - public function with() - { - $this->with = func_get_args(); - if (isset($this->with[0]) && is_array($this->with[0])) { - // the parameter is given as an array - $this->with = $this->with[0]; - } - return $this; - } - - public function index($column) - { - $this->index = $column; - return $this; - } - - public function tableAlias($value) - { - $this->tableAlias = $value; - return $this; - } - - public function scopes($names) - { - $this->scopes = $names; - return $this; - } -} diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 5162c41..0b3bb81 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -252,7 +252,7 @@ abstract class ActiveRecord extends Model */ public static function createActiveQuery() { - return new ActiveQuery(get_called_class()); + return new ActiveQuery(array('modelClass' => get_called_class())); } /** @@ -382,6 +382,35 @@ abstract class ActiveRecord extends Model } } + public function hasOne($class, $link) + { + return new HasOneRelation(array( + 'modelClass' => $class, + 'parentClass' => get_class($this), + 'link' => $link, + )); + } + + public function hasMany($class, $link) + { + return new HasManyRelation(array( + 'modelClass' => $class, + 'parentClass' => get_class($this), + 'link' => $link, + )); + } + + public function manyMany($class, $leftLink, $joinTable, $rightLink) + { + return new ManyManyRelation(array( + 'modelClass' => $class, + 'parentClass' => get_class($this), + 'leftLink' => $leftLink, + 'joinTable' => $joinTable, + 'rightLink' => $rightLink, + )); + } + /** * Initializes the internal storage for the relation. * This method is internally used by [[ActiveQuery]] when populating relation data. diff --git a/framework/db/ar/HasManyRelation.php b/framework/db/ar/HasManyRelation.php new file mode 100644 index 0000000..b62ed1d --- /dev/null +++ b/framework/db/ar/HasManyRelation.php @@ -0,0 +1,8 @@ +select !== $query->select) { - if (empty($this->select)) { - $this->select = $query->select; - } elseif (!empty($query->select)) { - $select1 = is_string($this->select) ? preg_split('/\s*,\s*/', trim($this->select), -1, PREG_SPLIT_NO_EMPTY) : $this->select; - $select2 = is_string($query->select) ? preg_split('/\s*,\s*/', trim($query->select), -1, PREG_SPLIT_NO_EMPTY) : $query->select; - $this->select = array_merge($select1, array_diff($select2, $select1)); - } - } - - if ($query->selectOption !== null) { - $this->selectOption = $query->selectOption; - } - - if ($query->distinct !== null) { - $this->distinct = $query->distinct; - } - - if ($query->from !== null) { - $this->from = $query->from; - } - - if ($query->limit !== null) { - $this->limit = $query->limit; - } - - if ($query->offset !== null) { - $this->offset = $query->offset; - } - - if ($query->where !== null) { - $this->andWhere($query->where); - } - - if ($query->having !== null) { - $this->andHaving($query->having); - } - - if ($query->params !== null) { - $this->addParams($query->params); - } - - if ($query->orderBy !== null) { - $this->addOrderBy($query->orderBy); - } - - if ($query->groupBy !== null) { - $this->addGroup($query->groupBy); - } - - if ($query->join !== null) { - if (empty($this->join)) { - $this->join = $query->join; - } else { - if (!is_array($this->join)) { - $this->join = array($this->join); - } - if (is_array($query->join)) { - $this->join = array_merge($this->join, $query->join); - } else { - $this->join[] = $query->join; - } + $properties = array( + 'select', 'selectOption', 'distinct', 'from', + 'where', 'limit', 'offset', 'orderBy', 'groupBy', + 'join', 'having', 'union', 'params', + ); + foreach ($properties as $name => $value) { + if ($value !== null) { + $this->$name = $value; } } - - if ($query->union !== null) { - if (empty($this->union)) { - $this->union = $query->union; - } else { - if (!is_array($this->union)) { - $this->union = array($this->union); - } - if (is_array($query->union)) { - $this->union = array_merge($this->union, $query->union); - } else { - $this->union[] = $query->union; - } - } - } - return $this; } } diff --git a/tests/unit/framework/db/ar/ActiveRecordTest.php b/tests/unit/framework/db/ar/ActiveRecordTest.php index 9b169eb..923345b 100644 --- a/tests/unit/framework/db/ar/ActiveRecordTest.php +++ b/tests/unit/framework/db/ar/ActiveRecordTest.php @@ -23,42 +23,14 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertTrue($result instanceof ActiveQuery); $customer = $result->one(); $this->assertTrue($customer instanceof Customer); - $this->assertEquals(1, $result->count); - $this->assertEquals(1, count($result)); // find all $result = Customer::find(); $customers = $result->all(); - $this->assertTrue(is_array($customers)); $this->assertEquals(3, count($customers)); $this->assertTrue($customers[0] instanceof Customer); $this->assertTrue($customers[1] instanceof Customer); $this->assertTrue($customers[2] instanceof Customer); - $this->assertEquals(3, $result->count); - $this->assertEquals(3, count($result)); - - // check count first - $result = Customer::find(); - $this->assertEquals(3, $result->count); - $customer = $result->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals(3, $result->count); - - // iterator - $result = Customer::find(); - $count = 0; - foreach ($result as $customer) { - $this->assertTrue($customer instanceof Customer); - $count++; - } - $this->assertEquals($count, $result->count); - - // array access - $result = Customer::find(); - $this->assertTrue($result[0] instanceof Customer); - $this->assertTrue($result[1] instanceof Customer); - $this->assertTrue($result[2] instanceof Customer); - $this->assertEquals(3, count($result)); // find by a single primary key $customer = Customer::find(2); @@ -70,7 +42,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertTrue($customer instanceof Customer); $this->assertEquals(2, $customer->id); - // find by Query + // find by Query array $query = array( 'where' => 'id=:id', 'params' => array(':id' => 2), @@ -80,13 +52,59 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertEquals('user2', $customer->name); // find count - $this->assertEquals(3, Customer::find()->count()); - $this->assertEquals(3, Customer::count()); - $this->assertEquals(2, Customer::count(array( - 'where' => 'id=1 OR id=2', - ))); - $this->assertEquals(2, Customer::count()->where('id=1 OR id=2')); +// $this->assertEquals(3, Customer::count()); +// $this->assertEquals(2, Customer::count(array( +// 'where' => 'id=1 OR id=2', +// ))); +// $this->assertEquals(2, Customer::find()->select('COUNT(*)')->where('id=1 OR id=2')->value()); + } + + public function testFindBySql() + { + // find one + $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user3', $customer->name); + + // find all + $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); + $this->assertEquals(3, count($customers)); + + // find with parameter binding + $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + } + + public function testScope() + { + $customers = Customer::find(array( + 'scopes' => array('active'), + ))->all(); + $this->assertEquals(2, count($customers)); + + $customers = Customer::find()->active()->all(); + $this->assertEquals(2, count($customers)); } +// +// public function testFindLazy() +// { +// $customer = Customer::find(2); +// $orders = $customer->orders; +// $this->assertEquals(2, count($orders)); +// +// $orders = $customer->orders()->where('id=3')->all(); +// $this->assertEquals(1, count($orders)); +// $this->assertEquals(3, $orders[0]->id); +// } +// +// public function testFindEager() +// { +// $customers = Customer::find()->with('orders')->all(); +// $this->assertEquals(3, count($customers)); +// $this->assertEquals(1, count($customers[0]->orders)); +// $this->assertEquals(2, count($customers[1]->orders)); +// } // public function testInsert() // { From de35c62c4eddbf2be8d3a39ee3b5cb895d91a64e Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 1 Dec 2012 15:52:27 -0500 Subject: [PATCH 4/4] RAR WIP --- framework/db/ar/ActiveQuery.php | 41 +++++++++++++++++--- framework/db/ar/ActiveRecord.php | 10 +++-- framework/db/ar/Relation.php | 10 +++-- framework/db/dao/QueryBuilder.php | 78 +++++++++++++++++++++++++++++---------- 4 files changed, 106 insertions(+), 33 deletions(-) diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index fbec6c9..39195a1 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -107,6 +107,34 @@ class ActiveQuery extends BaseQuery return $class::getDbConnection(); } + public function asArray($value = true) + { + $this->asArray = $value; + return $this; + } + + public function with() + { + $this->with = func_get_args(); + if (isset($this->with[0]) && is_array($this->with[0])) { + // the parameter is given as an array + $this->with = $this->with[0]; + } + return $this; + } + + public function index($column) + { + $this->index = $column; + return $this; + } + + public function scopes($names) + { + $this->scopes = $names; + return $this; + } + /** * Sets the parameters about query caching. * This is a shortcut method to {@link CDbConnection::cache()}. @@ -153,13 +181,14 @@ class ActiveQuery extends BaseQuery $command = $db->createCommand($this->sql, $this->params); $rows = $command->queryAll(); $records = $this->createRecords($rows); - - foreach ($this->with as $name => $config) { - $relation = $model->$name(); - foreach ($config as $p => $v) { - $relation->$p = $v; + if ($records !== array()) { + foreach ($this->with as $name => $config) { + $relation = $model->$name(); + foreach ($config as $p => $v) { + $relation->$p = $v; + } + $relation->findWith($records); } - $relation->findWith($records); } return $records; diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 0b3bb81..32861fe 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -68,11 +68,10 @@ abstract class ActiveRecord extends Model public static function model() { $className = get_called_class(); - if (isset(self::$_models[$className])) { - return self::$_models[$className]; - } else { - return self::$_models[$className] = new static; + if (!isset(self::$_models[$className])) { + self::$_models[$className] = new static; } + return self::$_models[$className]; } /** @@ -387,6 +386,7 @@ abstract class ActiveRecord extends Model return new HasOneRelation(array( 'modelClass' => $class, 'parentClass' => get_class($this), + 'parentRecords' => array($this), 'link' => $link, )); } @@ -396,6 +396,7 @@ abstract class ActiveRecord extends Model return new HasManyRelation(array( 'modelClass' => $class, 'parentClass' => get_class($this), + 'parentRecords' => array($this), 'link' => $link, )); } @@ -405,6 +406,7 @@ abstract class ActiveRecord extends Model return new ManyManyRelation(array( 'modelClass' => $class, 'parentClass' => get_class($this), + 'parentRecords' => array($this), 'leftLink' => $leftLink, 'joinTable' => $joinTable, 'rightLink' => $rightLink, diff --git a/framework/db/ar/Relation.php b/framework/db/ar/Relation.php index 50dba56..1362216 100644 --- a/framework/db/ar/Relation.php +++ b/framework/db/ar/Relation.php @@ -5,10 +5,14 @@ namespace yii\db\ar; class Relation extends ActiveQuery { public $parentClass; - public $parentRecords; - public function findWith($records) + public function findWith(&$parentRecords) { - + $this->andWhere(array('in', $links, $keys)); + $records = $this->find(); + foreach ($records as $record) { + // find the matching parent record(s) + // insert into the parent records(s) + } } } diff --git a/framework/db/dao/QueryBuilder.php b/framework/db/dao/QueryBuilder.php index bc5845a..e53dc79 100644 --- a/framework/db/dao/QueryBuilder.php +++ b/framework/db/dao/QueryBuilder.php @@ -469,16 +469,16 @@ class QueryBuilder extends \yii\base\Object public function buildCondition($condition) { static $builders = array( - 'and' => 'buildAndCondition', - 'or' => 'buildAndCondition', - 'between' => 'buildBetweenCondition', - 'not between' => 'buildBetweenCondition', - 'in' => 'buildInCondition', - 'not in' => 'buildInCondition', - 'like' => 'buildLikeCondition', - 'not like' => 'buildLikeCondition', - 'or like' => 'buildLikeCondition', - 'or not like' => 'buildLikeCondition', + 'AND' => 'buildAndCondition', + 'OR' => 'buildAndCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'NOT BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'NOT IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + 'NOT LIKE' => 'buildLikeCondition', + 'OR LIKE' => 'buildLikeCondition', + 'OR NOT LIKE' => 'buildLikeCondition', ); if (!is_array($condition)) { @@ -487,7 +487,7 @@ class QueryBuilder extends \yii\base\Object return ''; } if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = $condition[0]; + $operator = strtoupper($condition[0]); if (isset($builders[$operator])) { $method = $builders[$operator]; array_shift($condition); @@ -534,7 +534,6 @@ class QueryBuilder extends \yii\base\Object } } if ($parts !== array()) { - $operator = strtoupper($operator); return '(' . implode(") $operator (", $parts) . ')'; } else { return ''; @@ -570,22 +569,62 @@ class QueryBuilder extends \yii\base\Object $values = array($values); } - if ($values === array()) { + if ($values === array() || $column === array()) { return $operator === 'in' ? '0=1' : ''; } + if (is_array($column)) { + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values); + } else { + $column = reset($column); + foreach ($values as $i => $value) { + if (is_array($value)) { + $values[$i] = isset($value[$column]) ? $value[$column] : null; + } else { + $values[$i] = null; + } + } + } + } + foreach ($values as $i => $value) { - $values[$i] = is_string($value) ? $this->connection->quoteValue($value) : (string)$value; + if ($value === null) { + $values[$i] = 'NULL'; + } else { + $values[$i] = is_string($value) ? $this->connection->quoteValue($value) : (string)$value; + } } if (strpos($column, '(') === false) { $column = $this->quoteColumnName($column); } - $operator = strtoupper($operator); return "$column $operator (" . implode(', ', $values) . ')'; } + protected function buildCompositeInCondition($operator, $columns, $values) + { + foreach ($columns as $i => $column) { + if (strpos($column, '(') === false) { + $columns[$i] = $this->quoteColumnName($column); + } + } + $vss = array(); + foreach ($values as $value) { + $vs = array(); + foreach ($columns as $column) { + if (isset($value[$column])) { + $vs[] = is_string($value[$column]) ? $this->connection->quoteValue($value[$column]) : (string)$value[$column]; + } else { + $vs[] = 'NULL'; + } + } + $vss[] = '(' . implode(', ', $vs) . ')'; + } + return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; + } + private function buildLikeCondition($operator, $operands) { if (!isset($operands[0], $operands[1])) { @@ -599,21 +638,20 @@ class QueryBuilder extends \yii\base\Object } if ($values === array()) { - return $operator === 'like' || $operator === 'or like' ? '0=1' : ''; + return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; } - if ($operator === 'like' || $operator === 'not like') { + if ($operator === 'LIKE' || $operator === 'NOT LIKE') { $andor = ' AND '; } else { $andor = ' OR '; - $operator = $operator === 'or like' ? 'like' : 'not like'; + $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; } if (strpos($column, '(') === false) { $column = $this->quoteColumnName($column); } - $operator = strtoupper($operator); $parts = array(); foreach ($values as $value) { $parts[] = "$column $operator " . $this->connection->quoteValue($value); @@ -730,7 +768,7 @@ class QueryBuilder extends \yii\base\Object $table = $driver->quoteTableName($table); } } - $joins[$i] = strtoupper($join[0]) . ' ' . $table; + $joins[$i] = $join[0] . ' ' . $table; if (isset($join[2])) { $condition = $this->buildCondition($join[2]); if ($condition !== '') {