Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Sammaye committed Oct 4, 2016
1 parent 46560a7 commit 87ae49f
Show file tree
Hide file tree
Showing 4 changed files with 354 additions and 1 deletion.
228 changes: 228 additions & 0 deletions PagedQueryResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/

namespace sammaye\pq;

use Yii;
use yii\base\Object;

/**
* BatchQueryResult represents a batch query from which you can retrieve data in batches.
*
* You usually do not instantiate BatchQueryResult directly. Instead, you obtain it by
* calling [[Query::batch()]] or [[Query::each()]]. Because BatchQueryResult implements the [[\Iterator]] interface,
* you can iterate it to obtain a batch of data in each iteration. For example,
*
* ```php
* $query = (new Query)->from('user');
* foreach ($query->batch() as $i => $users) {
* // $users represents the rows in the $i-th batch
* }
* foreach ($query->each() as $user) {
* }
* ```
*
* @author Qiang Xue <[email protected]>
* @since 2.0
*/
class PagedQueryResult extends Object implements \Iterator
{
/**
* @var Connection the DB connection to be used when performing batch query.
* If null, the "db" application component will be used.
*/
public $db;
/**
* @var Query the query object associated with this batch query.
* Do not modify this property directly unless after [[reset()]] is called explicitly.
*/
public $query;
/**
* @var integer the number of rows to be returned in each batch.
*/
public $batchSize = 100;
/**
* @var boolean whether to return a single row during each iteration.
* If false, a whole batch of rows will be returned in each iteration.
*/
public $each = false;
/**
* @var boolean whether or not to paginate the query via offset and limit
* This is helpful for PHP7 where resuls sets actually add to a PHP's process
* memory usage by default
*/
public $page = false;

/**
* @var DataReader the data reader associated with this batch query.
*/
private $_dataReader;
/**
* @var array the data retrieved in the current batch
*/
private $_batch;
/**
* @var mixed the value for the current iteration
*/
private $_value;
/**
* @var string|integer the key for the current iteration
*/
private $_key;
/**
* @var integer holds the value of the offset for pagination
*/
private $_offset;
/**
* @var integer holds the limit of the query, if one is provided
*/
private $_limit;


/**
* Destructor.
*/
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}

/**
* Resets the batch query.
* This method will clean up the existing batch query so that a new batch query can be performed.
*/
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
$this->_offset = null;
$this->_limit = null;
}

/**
* Resets the iterator to the initial state.
* This method is required by the interface [[\Iterator]].
*/
public function rewind()
{
$this->reset();
$this->next();
}

/**
* Moves the internal pointer to the next dataset.
* This method is required by the interface [[\Iterator]].
*/
public function next()
{
if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) {
$this->_batch = $this->fetchData();
reset($this->_batch);
}

if ($this->each) {
$this->_value = current($this->_batch);
if ($this->query->indexBy !== null) {
$this->_key = key($this->_batch);
} elseif (key($this->_batch) !== null) {
$this->_key++;
} else {
$this->_key = null;
}
} else {
$this->_value = $this->_batch;
$this->_key = $this->_key === null ? 0 : $this->_key + 1;
}
}

/**
* Fetches the next batch of data.
* @return array the data fetched
*/
protected function fetchData()
{
if($this->_batch === null && $this->query->limit !== null){
// If it hasn't been fetched lets record the limit
$this->_limit = $this->query->limit;
}

if($this->page){

// If we are reaching near of the end of the predefined limit then
// let's sort that out
$batchSize = $this->batchSize;
if($this->_limit !== null){
if($this->_offset > $this->_limit){
$batchSize = 0;
}elseif($this->_offset === $this->_limit){
// Normally DB techs are exclusive in OFFSET
$batchSize = 1;
}elseif(($this->batchSize + $this->_offset) >= $this->_limit){
$batchSize = $this->_limit - $this->_offset;
}
}

$this->_dataReader = $this->query
->limit($batchSize)
->offset($this->_offset)
->createCommand($this->db)
->query();

}elseif($this->_dataReader === null){
$this->_dataReader = $this->query->createCommand($this->db)->query();
}

$rows = [];
$count = 0;
while ($count++ < $this->batchSize && ($row = $this->_dataReader->read())) {
$rows[] = $row;
}

if($this->page){
// If the result has been pulled without error then increment the offset
$this->_offset += $batchSize;
}

return $this->query->populate($rows);
}

/**
* Returns the index of the current dataset.
* This method is required by the interface [[\Iterator]].
* @return integer the index of the current row.
*/
public function key()
{
return $this->_key;
}

/**
* Returns the current dataset.
* This method is required by the interface [[\Iterator]].
* @return mixed the current dataset.
*/
public function current()
{
return $this->_value;
}

/**
* Returns whether there is a valid dataset at the current position.
* This method is required by the interface [[\Iterator]].
* @return boolean whether there is a valid dataset at the current position.
*/
public function valid()
{
return !empty($this->_batch);
}
}
71 changes: 71 additions & 0 deletions Query.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace sammaye\pq;

use Yii;
use yii\db\Query as BaseQuery;
use PagedQueryResult;

class Query extends BaseQuery
{
/**
* Starts a batch query.
*
* A batch query supports fetching data in batches, which can keep the memory usage under a limit.
* This method will return a [[BatchQueryResult]] object which implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*
* For example,
*
* ```php
* $query = (new Query)->from('user');
* foreach ($query->batch() as $rows) {
* // $rows is an array of 10 or fewer rows from user table
* }
* ```
*
* @param integer $batchSize the number of records to be fetched in each batch.
* @param Connection $db the database connection. If not set, the "db" application component will be used.
* @return BatchQueryResult the batch query result. It implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*/
public function batch($batchSize = 100, $page = true, $db = null)
{
return Yii::createObject([
'class' => PagedQueryResult::className(),
'query' => $this,
'batchSize' => $batchSize,
'db' => $db,
'each' => false,
'page' => $page
]);
}

/**
* Starts a batch query and retrieves data row by row.
* This method is similar to [[batch()]] except that in each iteration of the result,
* only one row of data is returned. For example,
*
* ```php
* $query = (new Query)->from('user');
* foreach ($query->each() as $row) {
* }
* ```
*
* @param integer $batchSize the number of records to be fetched in each batch.
* @param Connection $db the database connection. If not set, the "db" application component will be used.
* @return BatchQueryResult the batch query result. It implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*/
public function each($batchSize = 100, $page = true, $db = null)
{
return Yii::createObject([
'class' => PagedQueryResult::className(),
'query' => $this,
'batchSize' => $batchSize,
'db' => $db,
'each' => true,
'page' => $page
]);
}
}
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,38 @@
# yii2-pagedquery
# yii2-pq
A paged query class for Yii2 to deal with PHP7 changed in mysqlnd

It will load your result set the same way as pagination works using `OFFSET` and `LIMIT`.

This extension is 99% for MySQL users.

## Installation

Just include it in your composer:

php composer require "sammaye/yii2-pq":"~1.0.0"

## Usage

$query = (new \sammaye\pq\Query)
->from(Title::tableName())
->where('live=1')
->limit(300)
->orderBy(['id' => SORT_DESC]);
foreach($query->each() as $k => $v){

And it will return in batches of 100 up to 300.

As you can see there is not much to learn about this extension except how to include it.

**Note:** There is no active record part to this query currently due to the
nature of PHP class inheritance and inclusion which means I would have to copy the `ActiveQuery` entirely.

## Why?

I noticed that many of my cronjobs failed after an upgrade to PHP7. It was not
long before I realised that there was two changes since PHP5.4:

- The default MySQL driver used by PHP7 has changed to mysqlnd
- And, mysqlnd now adds your [result sets to it's own memory](http://php.net/manual/en/mysqlinfo.concepts.buffering.php), buffering queries

Added to that, my own observations that unbuffered queries suck meant that I created this.
18 changes: 18 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "sammaye/yii2-pq",
"description": "A paged query class for Yii2 to deal with PHP7 changed in mysqlnd",
"keywords": ["yii2", "pagination", "query", "db"],
"type": "yii2-extension",
"authors": [
{
"name": "Sammaye",
"homepage": "https://www.sammaye.com"
}
],
"require": {
"php": ">=5.4.0"
},
"autoload": {
"psr-4": {"sammaye\\pq\\": ""}
}
}

0 comments on commit 87ae49f

Please sign in to comment.