Browse Source

Merge branch 'console-color'

tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
e3d57f0cf9
  1. 454
      framework/console/Controller.php
  2. 479
      framework/db/ar/ActiveQuery.php
  3. 41
      framework/db/ar/ActiveRecord.php
  4. 8
      framework/db/ar/HasManyRelation.php
  5. 8
      framework/db/ar/HasOneRelation.php
  6. 10
      framework/db/ar/ManyManyRelation.php
  7. 18
      framework/db/ar/Relation.php
  8. 100
      framework/db/dao/BaseQuery.php
  9. 78
      framework/db/dao/QueryBuilder.php
  10. 88
      tests/unit/framework/db/ar/ActiveRecordTest.php

454
framework/console/Controller.php

@ -24,10 +24,42 @@ use yii\base\Exception;
* ~~~ * ~~~
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
class Controller extends \yii\base\Controller 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. * This method is invoked when the request parameters do not satisfy the requirement of the specified action.
* The default implementation will throw an exception. * The default implementation will throw an exception.
@ -112,4 +144,426 @@ class Controller extends \yii\base\Controller
$input = trim(fgets(STDIN)); $input = trim(fgets(STDIN));
return empty($input) ? $default : !strncasecmp($input, 'y', 1); 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 used in windows.
* @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 used in windows.
* @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 DECTCEM code ?25l to the terminal.
* Use {@link showCursor} to bring it back.
* Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit.
*/
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.
* **Note:** 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 the line, the cursor is currently on 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 position to the beginning of the line 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 position to the end of the line by sending ANSI code EL with argument 0 to the terminal.
* Cursor position will not be changed.
*/
public function clearLineAfterCursor()
{
echo "\033[0K";
}
/**
* Will send ANSI format for following output
*
* You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg
* TODO: documentation
*/
public function ansiStyle()
{
echo "\033[" . implode(';', func_get_args()) . 'm';
}
/**
* Will return a string formatted with the given ANSI style
*
* See {@link ansiStyle} for possible arguments.
* @param string $string the string to be formatted
* @return string
*/
public function ansiStyleString($string)
{
$args = func_get_args();
array_shift($args);
$code = implode(';', $args);
return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m";
}
//const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors
public function xterm256ColorFg($i) // TODO naming!
{
return '38;5;'.$i;
}
public function xterm256ColorBg($i) // TODO naming!
{
return '48;5;'.$i;
}
/**
* Usage: list($w, $h) = $this->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()
{
echo "\033[0m";
}
/**
* 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)
{
$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 .= '</span>';
}
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 '<span' . (!empty($styleString) ? 'style="' . implode(';', $styleString) : '') . '>';
}, $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.
* <pre>
* 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 %
* </pre>
* 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);
}
} }

479
framework/db/ar/ActiveQuery.php

@ -15,7 +15,7 @@ use yii\base\VectorIterator;
use yii\db\dao\Expression; use yii\db\dao\Expression;
use yii\db\Exception; use yii\db\Exception;
class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, \Countable class ActiveQuery extends BaseQuery
{ {
/** /**
* @var string the name of the ActiveRecord class. * @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 * @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. * @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. * 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 * @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. * @var string the SQL statement to be executed for retrieving AR records.
* This is set by [[ActiveRecord::findBySql()]]. * This is set by [[ActiveRecord::findBySql()]].
*/ */
public $sql; 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) public function __call($name, $params)
{ {
@ -77,7 +61,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
*/ */
public function all() public function all()
{ {
return $this->findRecords(); return $this->find();
} }
/** /**
@ -88,8 +72,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
*/ */
public function one() public function one()
{ {
$this->limit = 1; $records = $this->find();
$records = $this->findRecords();
return isset($records[0]) ? $records[0] : null; return isset($records[0]) ? $records[0] : null;
} }
@ -124,13 +107,32 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
return $class::getDbConnection(); return $class::getDbConnection();
} }
/** public function asArray($value = true)
* Returns the number of items in the vector. {
* @return integer the number of items in the vector $this->asArray = $value;
*/ return $this;
public function getCount() }
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)
{ {
return $this->count(); $this->index = $column;
return $this;
}
public function scopes($names)
{
$this->scopes = $names;
return $this;
} }
/** /**
@ -150,139 +152,46 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
return $this; return $this;
} }
/** protected function find()
* 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()
{ {
$modelClass = $this->modelClass;
/** /**
* find the primary ARs * @var ActiveRecord $model
* for each child relation
* find the records filtered by the PK constraints
* populate primary ARs with the related records
* recursively call this metod again
*/ */
} $model = $modelClass::model();
/**
protected function findByParent($parent) * @var \yii\db\dao\Connection $db
{ */
$db = $model->getDbConnection();
}
protected function findRecords()
{
if (!empty($this->with)) {
return $this->findWithRelations();
}
if ($this->sql === null) { if ($this->sql === null) {
if ($this->from === null) { if ($this->from === null) {
$modelClass = $this->modelClass; $tableName = $model->getTableSchema()->name;
$tableName = $modelClass::model()->getTableSchema()->name;
$this->from = array($tableName); $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(); $rows = $command->queryAll();
return $this->createRecords($rows); $records = $this->createRecords($rows);
} if ($records !== array()) {
foreach ($this->with as $name => $config) {
$relation = $model->$name();
foreach ($config as $p => $v) {
$relation->$p = $v;
}
$relation->findWith($records);
}
}
protected function findWithRelations() return $records;
{
$records = $this->findRecords();
} }
protected function createRecords($rows) protected function createRecords($rows)
@ -311,271 +220,3 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess,
return $records; return $records;
} }
} }
/**
* 1. eager loading, base limited and has has_many relations
* 2.
* ActiveFinder.php is ...
*
* @property integer $count
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @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;
}
}

41
framework/db/ar/ActiveRecord.php

@ -68,11 +68,10 @@ abstract class ActiveRecord extends Model
public static function model() public static function model()
{ {
$className = get_called_class(); $className = get_called_class();
if (isset(self::$_models[$className])) { if (!isset(self::$_models[$className])) {
return self::$_models[$className]; self::$_models[$className] = new static;
} else {
return self::$_models[$className] = new static;
} }
return self::$_models[$className];
} }
/** /**
@ -252,7 +251,7 @@ abstract class ActiveRecord extends Model
*/ */
public static function createActiveQuery() public static function createActiveQuery()
{ {
return new ActiveQuery(get_called_class()); return new ActiveQuery(array('modelClass' => get_called_class()));
} }
/** /**
@ -382,6 +381,38 @@ abstract class ActiveRecord extends Model
} }
} }
public function hasOne($class, $link)
{
return new HasOneRelation(array(
'modelClass' => $class,
'parentClass' => get_class($this),
'parentRecords' => array($this),
'link' => $link,
));
}
public function hasMany($class, $link)
{
return new HasManyRelation(array(
'modelClass' => $class,
'parentClass' => get_class($this),
'parentRecords' => array($this),
'link' => $link,
));
}
public function manyMany($class, $leftLink, $joinTable, $rightLink)
{
return new ManyManyRelation(array(
'modelClass' => $class,
'parentClass' => get_class($this),
'parentRecords' => array($this),
'leftLink' => $leftLink,
'joinTable' => $joinTable,
'rightLink' => $rightLink,
));
}
/** /**
* Initializes the internal storage for the relation. * Initializes the internal storage for the relation.
* This method is internally used by [[ActiveQuery]] when populating relation data. * This method is internally used by [[ActiveQuery]] when populating relation data.

8
framework/db/ar/HasManyRelation.php

@ -0,0 +1,8 @@
<?php
namespace yii\db\ar;
class HasManyRelation extends Relation
{
public $link;
}

8
framework/db/ar/HasOneRelation.php

@ -0,0 +1,8 @@
<?php
namespace yii\db\ar;
class HasOneRelation extends Relation
{
public $link;
}

10
framework/db/ar/ManyManyRelation.php

@ -0,0 +1,10 @@
<?php
namespace yii\db\ar;
class ManyManyRelation extends Relation
{
public $joinTable;
public $leftLink;
public $rightLink;
}

18
framework/db/ar/Relation.php

@ -0,0 +1,18 @@
<?php
namespace yii\db\ar;
class Relation extends ActiveQuery
{
public $parentClass;
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)
}
}
}

100
framework/db/dao/BaseQuery.php

@ -532,103 +532,23 @@ class BaseQuery extends \yii\base\Component
/** /**
* Merges this query with another one. * Merges this query with another one.
* * If a property of `$query` is not null, it will be used to overwrite
* The merging is done according to the following rules: * the corresponding property of `$this`.
*
* - [[select]]: the union of both queries' [[select]] property values.
* - [[selectOption]], [[distinct]], [[from]], [[limit]], [[offset]]: the new query
* takes precedence over this query.
* - [[where]], [[having]]: the new query's corresponding property value
* will be 'AND' together with the existing one.
* - [[params]], [[orderBy]], [[groupBy]], [[join]], [[union]]: the new query's
* corresponding property value will be appended to the existing one.
*
* In general, the merging makes the resulting query more restrictive and specific.
* @param BaseQuery $query the new query to be merged with this query. * @param BaseQuery $query the new query to be merged with this query.
* @return BaseQuery the query object itself * @return BaseQuery the query object itself
*/ */
public function mergeWith(BaseQuery $query) public function mergeWith(BaseQuery $query)
{ {
if ($this->select !== $query->select) { $properties = array(
if (empty($this->select)) { 'select', 'selectOption', 'distinct', 'from',
$this->select = $query->select; 'where', 'limit', 'offset', 'orderBy', 'groupBy',
} elseif (!empty($query->select)) { 'join', 'having', 'union', 'params',
$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; foreach ($properties as $name => $value) {
$this->select = array_merge($select1, array_diff($select2, $select1)); if ($value !== null) {
} $this->$name = $value;
}
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;
}
} }
} }
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; return $this;
} }
} }

78
framework/db/dao/QueryBuilder.php

@ -469,16 +469,16 @@ class QueryBuilder extends \yii\base\Object
public function buildCondition($condition) public function buildCondition($condition)
{ {
static $builders = array( static $builders = array(
'and' => 'buildAndCondition', 'AND' => 'buildAndCondition',
'or' => 'buildAndCondition', 'OR' => 'buildAndCondition',
'between' => 'buildBetweenCondition', 'BETWEEN' => 'buildBetweenCondition',
'not between' => 'buildBetweenCondition', 'NOT BETWEEN' => 'buildBetweenCondition',
'in' => 'buildInCondition', 'IN' => 'buildInCondition',
'not in' => 'buildInCondition', 'NOT IN' => 'buildInCondition',
'like' => 'buildLikeCondition', 'LIKE' => 'buildLikeCondition',
'not like' => 'buildLikeCondition', 'NOT LIKE' => 'buildLikeCondition',
'or like' => 'buildLikeCondition', 'OR LIKE' => 'buildLikeCondition',
'or not like' => 'buildLikeCondition', 'OR NOT LIKE' => 'buildLikeCondition',
); );
if (!is_array($condition)) { if (!is_array($condition)) {
@ -487,7 +487,7 @@ class QueryBuilder extends \yii\base\Object
return ''; return '';
} }
if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
$operator = $condition[0]; $operator = strtoupper($condition[0]);
if (isset($builders[$operator])) { if (isset($builders[$operator])) {
$method = $builders[$operator]; $method = $builders[$operator];
array_shift($condition); array_shift($condition);
@ -534,7 +534,6 @@ class QueryBuilder extends \yii\base\Object
} }
} }
if ($parts !== array()) { if ($parts !== array()) {
$operator = strtoupper($operator);
return '(' . implode(") $operator (", $parts) . ')'; return '(' . implode(") $operator (", $parts) . ')';
} else { } else {
return ''; return '';
@ -568,22 +567,62 @@ class QueryBuilder extends \yii\base\Object
$values = (array)$values; $values = (array)$values;
if ($values === array()) { if ($values === array() || $column === array()) {
return $operator === 'in' ? '0=1' : ''; 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) { 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) { if (strpos($column, '(') === false) {
$column = $this->quoteColumnName($column); $column = $this->quoteColumnName($column);
} }
$operator = strtoupper($operator);
return "$column $operator (" . implode(', ', $values) . ')'; 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) private function buildLikeCondition($operator, $operands)
{ {
if (!isset($operands[0], $operands[1])) { if (!isset($operands[0], $operands[1])) {
@ -595,21 +634,20 @@ class QueryBuilder extends \yii\base\Object
$values = (array)$values; $values = (array)$values;
if ($values === array()) { 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 '; $andor = ' AND ';
} else { } else {
$andor = ' OR '; $andor = ' OR ';
$operator = $operator === 'or like' ? 'like' : 'not like'; $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE';
} }
if (strpos($column, '(') === false) { if (strpos($column, '(') === false) {
$column = $this->quoteColumnName($column); $column = $this->quoteColumnName($column);
} }
$operator = strtoupper($operator);
$parts = array(); $parts = array();
foreach ($values as $value) { foreach ($values as $value) {
$parts[] = "$column $operator " . $this->connection->quoteValue($value); $parts[] = "$column $operator " . $this->connection->quoteValue($value);
@ -726,7 +764,7 @@ class QueryBuilder extends \yii\base\Object
$table = $driver->quoteTableName($table); $table = $driver->quoteTableName($table);
} }
} }
$joins[$i] = strtoupper($join[0]) . ' ' . $table; $joins[$i] = $join[0] . ' ' . $table;
if (isset($join[2])) { if (isset($join[2])) {
$condition = $this->buildCondition($join[2]); $condition = $this->buildCondition($join[2]);
if ($condition !== '') { if ($condition !== '') {

88
tests/unit/framework/db/ar/ActiveRecordTest.php

@ -23,42 +23,14 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertTrue($result instanceof ActiveQuery); $this->assertTrue($result instanceof ActiveQuery);
$customer = $result->one(); $customer = $result->one();
$this->assertTrue($customer instanceof Customer); $this->assertTrue($customer instanceof Customer);
$this->assertEquals(1, $result->count);
$this->assertEquals(1, count($result));
// find all // find all
$result = Customer::find(); $result = Customer::find();
$customers = $result->all(); $customers = $result->all();
$this->assertTrue(is_array($customers));
$this->assertEquals(3, count($customers)); $this->assertEquals(3, count($customers));
$this->assertTrue($customers[0] instanceof Customer); $this->assertTrue($customers[0] instanceof Customer);
$this->assertTrue($customers[1] instanceof Customer); $this->assertTrue($customers[1] instanceof Customer);
$this->assertTrue($customers[2] 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 // find by a single primary key
$customer = Customer::find(2); $customer = Customer::find(2);
@ -70,7 +42,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertTrue($customer instanceof Customer); $this->assertTrue($customer instanceof Customer);
$this->assertEquals(2, $customer->id); $this->assertEquals(2, $customer->id);
// find by Query // find by Query array
$query = array( $query = array(
'where' => 'id=:id', 'where' => 'id=:id',
'params' => array(':id' => 2), 'params' => array(':id' => 2),
@ -80,13 +52,59 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertEquals('user2', $customer->name); $this->assertEquals('user2', $customer->name);
// find count // find count
$this->assertEquals(3, Customer::find()->count()); // $this->assertEquals(3, Customer::count());
$this->assertEquals(3, Customer::count()); // $this->assertEquals(2, Customer::count(array(
$this->assertEquals(2, Customer::count(array( // 'where' => 'id=1 OR id=2',
'where' => 'id=1 OR id=2', // )));
))); // $this->assertEquals(2, Customer::find()->select('COUNT(*)')->where('id=1 OR id=2')->value());
$this->assertEquals(2, Customer::count()->where('id=1 OR id=2')); }
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() // public function testInsert()
// { // {

Loading…
Cancel
Save