Database/ModelCollection.php
<?php
namespace Colibri\Database;
use Colibri\Base\DynamicCollection;
use Colibri\Base\DynamicCollectionInterface;
use Colibri\Database;
/**
* Абстрактный класс ModelCollection.
*
* Класс основан на DynamicCollection, по сему свои элементы подгружает
* только тогда, когда идёт первое обращение к элементу коллекции.
*
* @property mixed $parentID
*/
abstract class ModelCollection extends DynamicCollection implements DynamicCollectionInterface
{
/** @var string|\Colibri\Database\Model */
protected static $itemClass = 'itemClass_not_set';
/** @var array */
protected $FKName = ['_id', '_id'];
/** @var array */
protected $FKValue = [null, null];
/** @var mixed */
protected $_parentID;
/** @var array */
protected $itemFields = [];
/** @var array */
protected $itemFieldTypes = [];
/** @var Query */
private $query = null;
/** @var bool */
private $pagedQuery = false;
/** @var int */
public $recordsPerPage = 20;
/** @var int */
public $recordsCount = null;
/** @var int */
public $pagesCount = null;
/**
* @param mixed $parentID
*
* @throws \Colibri\Database\DbException
*/
public function __construct($parentID = null)
{
$this->parentID = $parentID;
$this->getFieldsAndTypes();
}
/**
* @return Query
*/
protected function getQuery(): Query
{
return $this->query ?? $this->query = $this->query();
}
/**
* @return \Colibri\Database\Query
*/
abstract protected function query(): Query;
/**
* DynamicCollectionInterface ::fillItems() implementation.
*
* @param array $rows
*
* @return bool
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
public function fillItems(array &$rows = null)
{
if ($rows === null) {
return $this->load();
}
$this->clearItems();
foreach ($rows as $row) {
$item = $this->instantiateItem($row);
$this->addItem($item);
}
return true;
}
/**
* @return Query
*
* @throws \Colibri\Database\DbException
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
abstract protected function selFromDbAllQuery(): Query;
/**
* @param string $propertyName
*
* @return mixed
*
* @throws \UnexpectedValueException
*/
public function __get($propertyName)
{
switch ($propertyName) {
case 'parentID':
return $this->FKValue[0];
default:
throw new \UnexpectedValueException('property ' . $propertyName . ' not defined in class ' . static::class);
}
}
/**
* @param string $propertyName
* @param mixed $propertyValue
*
* @return mixed
*
* @throws \UnexpectedValueException
*/
public function __set($propertyName, $propertyValue)
{
switch ($propertyName) {
case 'parentID':
return $this->FKValue[0] = $propertyValue;
default:
throw new \UnexpectedValueException('property ' . $propertyName . ' not defined in class ' . static::class);
}
}
// with Items
///////////////////////////////////////////////////////////////////////////
/**
* @param int $position
*
* @throws \OutOfBoundsException
*/
final protected function shiftLeftFromPos($position)
{
$cnt = parent::count();
if ($position < 1 || $position >= $cnt) {
throw new \OutOfBoundsException('position to shift from must be in range 1..Length-1');
}
for ($i = $position; $i < $cnt; $i++) {
$this->items[$i - 1] = $this->items[$i];
}
}
/**
* @param \Colibri\Database\Model $object
*/
protected function addItem(Model &$object)
{
$this->items[] = $object;
}
/**
* @param int $itemID
*
* @return bool|\Colibri\Database\Model
*
* @throws \OutOfBoundsException
*/
protected function delItem($itemID)
{
$pos = $this->indexOf($itemID);
if ($pos == -1) {
return false;
}
$item = $this->items[$pos];
if ($pos != count($this->items) - 1) {
$this->shiftLeftFromPos($pos + 1);
}
array_pop($this->items);
return $item;
}
/**
* @return void
*/
protected function clearItems()
{
$this->items = [];
}
/**
* @param array $row
*
* @return \Colibri\Database\Model
*/
protected function instantiateItem(array $row)
{
return new static::$itemClass($row);
}
///////////////////////////////////////////////////////////////////////////
// with DataBase
///////////////////////////////////////////////////////////////////////////
/**
* @param \Colibri\Database\Model $id
*/
abstract protected function addToDb(Database\Model &$id);
/**
* @param mixed $id
*/
abstract protected function delFromDb($id);
/**
* @return array
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
protected function selFromDbAll()
{
$selectedRows = $this->doQuery($this->selFromDbAllQuery())->fetchAll();
$this->query = null;
// TODO [alek13]: bring it out
if ($this->pagedQuery) {
$row = static::db()->getConnection()->query('SELECT FOUND_ROWS()')->fetch();
$this->recordsCount = reset($row);
$this->pagesCount = ceil($this->recordsCount / $this->recordsPerPage);
}
return $selectedRows;
}
/**
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
*/
abstract protected function delFromDbAll();
///////////////////////////////////////////////////////////////////////////
/**
* @param Query $query
*
* @return bool|\Colibri\Database\AbstractDb\Driver\Query\ResultInterface
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \UnexpectedValueException
*/
protected function doQuery(Query $query)
{
return static::db()->query($query);
}
/**
* @throws \Colibri\Database\DbException
*/
protected function getFieldsAndTypes()
{
if (empty($this->itemFields)) {
$metadata = static::db()->metadata()->getColumnsMetadata(static::$itemClass::getTableName());
$this->itemFields = &$metadata['fields'];
$this->itemFieldTypes = &$metadata['fieldTypes'];
}
}
/**
* @return int
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \UnexpectedValueException
*/
public function count(): int
{
return $this->items !== null
? parent::count()
: (int)current(
static::db()->query(
(clone $this->selFromDbAllQuery())->count()
)->fetch()
);
}
///////////////////////////////////////////////////////////////////////////
/**
* @param array $where array('field [op]' => value, ...)
* @param string $type one of 'and'|'or'
*
* @return $this|\Colibri\Database\ModelCollection|Model[]
*
* @throws \InvalidArgumentException
*/
final public function where(array $where, $type = 'and')
{
$this->getQuery()->where($where, $type);
return $this;
}
/**
* @param array $plan
*
* @return ModelCollection|$this|Model[]
*/
final public function wherePlan(array $plan)
{
$this->getQuery()->wherePlan($plan);
return $this;
}
/**
* @param array $orderBy array('field1'=>'orientation','field2'=>'orientation'), 'fieldN' - name of field,
* 'orientation' - ascending or descending abbreviation ('asc' or 'desc')
*
* @return ModelCollection|$this|Model[]
*/
final public function orderBy(array $orderBy)
{
$this->getQuery()->orderBy($orderBy);
return $this;
}
/**
* @param int $offsetOrCount
* @param int $count
*
* @return ModelCollection|$this|Model[]
*/
final public function limit($offsetOrCount, $count = null)
{
$this->getQuery()->limit($offsetOrCount, $count);
$this->pagedQuery = true;
return $this;
}
/**
* @param int $pageNumber 0..N
* @param int $recordsPerPage
*
* @return ModelCollection|$this|Model[]
*/
final public function page($pageNumber, $recordsPerPage = null)
{
$recordsPerPage = $recordsPerPage ?? $this->recordsPerPage;
$this->getQuery()->limit(((int)$pageNumber) * $recordsPerPage, $recordsPerPage);
$this->pagedQuery = true;
return $this;
}
///////////////////////////////////////////////////////////////////////////
/**
* @param \Colibri\Database\Model $object
*
* @return bool
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
public function add(Database\Model $object)
{
if ($this->items === null) {
if ( ! $this->fillItems()) {
return false;
}
}
$this->addToDb($object);
$this->addItem($object);
return true;
}
/**
* @param mixed $itemID
*
* @return bool|\Colibri\Database\Model
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \InvalidArgumentException
* @throws \OutOfBoundsException
* @throws \UnexpectedValueException
*/
public function remove($itemID)
{
if ($this->items === null) {
if ( ! $this->fillItems()) {
return false;
}
}
$this->delFromDb($itemID);
return $this->delItem($itemID);
}
/**
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
*/
public function clear()
{
$this->delFromDbAll();
$this->clearItems();
}
/**
* @param mixed $parentID
*
* @return bool
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
public function load($parentID = null)
{
if ($parentID !== null) {
$this->parentID = $parentID;
}
if ( ! is_array($rows = $this->selFromDbAll())) {
return false;
}
if ( ! $this->fillItems($rows)) {
return false;
}
return true;
}
/**
* @return bool
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
public function reload()
{
return $this->load();
}
/**
* @param int $itemID
*
* @return int
*/
public function indexOf($itemID)
{
$cnt = parent::count();
for ($i = 0; $i < $cnt; $i++) {
if ($this->items[$i]->id == $itemID) {
return $i;
}
}
return -1;
}
/**
* @param int $itemID
*
* @return bool
*/
public function contains($itemID)
{
if ($this->indexOf($itemID) == -1) {
return false;
}
return true;
}
/**
* @param int|string $id
*
* @return bool
*/
public function &getItemByID($id)
{
if ( ! $count = parent::count()) {
return false;
}
/** @var \Colibri\Database\Model $itemClass */
$itemClass = static::$itemClass;
/** @noinspection PhpUndefinedVariableInspection */
$PKfn = $itemClass::$PKFieldName[0];
for ($i = 0; $i < $count; $i++) {
if (isset($this->items[$i]->$PKfn) && $this->items[$i]->$PKfn == $id) {
return $this->items[$i];
}
}
return false;
}
/**
* @param string $fieldName which field push to an array
* @param string $keyField which field use as keys of array
*
* @return array
*/
public function &toArrayOf($fieldName, $keyField = null)
{
$arr = [];
foreach ($this as $object) {
if ($keyField === null) {
$arr[] = $object->$fieldName;
} else {
$arr[$object->$keyField] = $object->$fieldName;
}
}
return $arr;
}
/**
* @param string $fieldName
* @param string $glue
*
* @return string
*/
public function implode($fieldName, $glue = ', ')
{
return implode($glue, $this->toArrayOf($fieldName));
}
/**
* @return \Colibri\Database\AbstractDb\DriverInterface
*
* @throws \Colibri\Database\DbException
*/
protected static function db()
{
return static::$itemClass::db();
}
///////////////////////////////////////////////////////////////////////////
/**
* @param string $fieldName
* @param string $keyField
*
* @return static|ModelCollection|Model[]|array
*
* @throws \Colibri\Database\DbException
*/
public static function &all($fieldName = null, $keyField = null)
{
$collection = new static();
if ($fieldName !== null) {
return $collection->toArrayOf($fieldName, $keyField);
}
return $collection;
}
/**
* @return $this
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
public function get()
{
if ( ! $this->load()) {
throw new DbException('failed to load collection');
}
return $this;
}
/**
* @param callable $handler
*
* @throws \Colibri\Database\DbException
* @throws \Colibri\Database\Exception\SqlException
* @throws \UnexpectedValueException
*/
public function walk(callable $handler)
{
$cursor = static::db()->query($this->selFromDbAllQuery())->cursor();
foreach ($cursor as $row) {
if ($handler($this->instantiateItem($row)) === false) {
break;
}
}
}
}