Asymptix/Framework

View on GitHub
framework/db/DBObject.php

Summary

Maintainability
D
1 day
Test Coverage
<?php

namespace Asymptix\db;

use Asymptix\core\Tools;

/**
 * DBObject class. Object oriented representation of DB record.
 *
 * @category Asymptix PHP Framework
 * @author Dmytro Zarezenko <dmytro.zarezenko@gmail.com>
 * @copyright (c) 2009 - 2018, Dmytro Zarezenko
 *
 * @git https://github.com/Asymptix/Framework
 * @license http://opensource.org/licenses/MIT
 */
abstract class DBObject extends \Asymptix\core\BasicObject {

    /**
     * Status constants.
     */
    const STATUS_ACTIVATED = 1;
    const STATUS_DEACTIVATED = 0;

    const STATUS_REMOVED = 1;
    const STATUS_RESTORED = 0;

    /**
     * DB Query object for Prepared Statement.
     *
     * @var DBPreparedQuery
     */
    private $dbQuery = null;

    /**
     * Creates new default object.
     */
    public function __construct() {}

    /**
     * Returns primary key value.
     *
     * @return mixed.
     */
    public function getId() {
        if (is_null(static::ID_FIELD_NAME)) {
            return null;
        }

        return $this->getFieldValue(static::ID_FIELD_NAME);
    }

    /**
     * Sets primary key value.
     *
     * @param mixed $recordId Key vaue.
     *
     * @return bool Success flag.
     * @throws DBCoreException If object has no field with such name.
     */
    public function setId($recordId) {
        return $this->setFieldValue(static::ID_FIELD_NAME, $recordId);
    }

    /**
     * Returns name of the primary key field.
     *
     * @return mixed
     */
    public static function getIdFieldName() {
        return static::ID_FIELD_NAME;
    }

    /**
     * Returns DBObject table name.
     *
     * @return string
     */
    public static function getTableName() {
        return static::TABLE_NAME;
    }

    /**
     * Saves activation flag to the database.
     *
     * @return int Returns the number of affected rows on success, and -1 if
     *            the last query failed.
     */
    public function saveActivationFlag() {
        return DBCore::doUpdateQuery(
            "UPDATE " . static::TABLE_NAME . "
                SET activation = ?
             WHERE " . static::ID_FIELD_NAME . " = ?
             LIMIT 1",
            "ii",
            [$this->activation, $this->id]
        );
    }

    /**
     * Detects if current record is activated.
     *
     * @return bool
     *
     * @throws DBCoreException If record hos no 'activation' field.
     */
    public function isActivated() {
        $activation = $this->getFieldValue('activation');
        if (is_null($activation)) {
            throw new DBCoreException("This object has no parameter 'activation'");
        }

        return ($activation > 0);
    }

    /**
     * Activates record and save changes into the database.
     *
     * @return int
     */
    public function activate() {
        $this->setFieldValue('activation', self::STATUS_ACTIVATED);

        return $this->saveActivationFlag();
    }

    /**
     * Deactivates record and save changes into the database.
     *
     * @return int
     */
    public function deactivate() {
        $this->setFieldValue('activation', self::STATUS_DEACTIVATED);

        return $this->saveActivationFlag();
    }

    /**
     * Changes record activation flag and save changes into the database.
     */
    public function changeActivation() {
        if ($this->isActivated()) {
            $this->deactivate();
        } else {
            $this->activate();
        }
    }

    /**
     * Saves removement flag to the database.
     *
     * @return int Returns the number of affected rows on success, and -1 if
     *            the last query failed.
     */
    public function saveRemovementFlag() {
        return DBCore::doUpdateQuery(
            "UPDATE " . static::TABLE_NAME . "
                SET removed = ?
             WHERE " . static::ID_FIELD_NAME . " = ?
             LIMIT 1",
            "ii",
            [$this->removed, $this->id]
        );
    }

    /**
     * Detects if current record is removed.
     *
     * @return bool
     *
     * @throws DBCoreException If record hos no 'removed' field.
     */
    public function isRemoved() {
        $isRemoved = $this->getFieldValue('removed');
        if (is_null($isRemoved)) {
            throw new DBCoreException("This object has no parameter 'removed'");
        }

        return ($isRemoved == self::STATUS_REMOVED);
    }

    /**
     * Enable removed flag of the record and save changes into the database.
     *
     * @return int
     */
    public function remove() {
        $this->setFieldValue('removed', self::STATUS_REMOVED);

        return $this->saveRemovementFlag();
    }

    /**
     * Disable removed flag of the record and save changes into the database.
     *
     * @return int
     */
    public function restore() {
        $this->setFieldValue('removed', self::STATUS_RESTORED);

        return $this->saveRemovementFlag();
    }

    /**
     * Changes record removement flag and save changes into the database.
     */
    public function changeRemovement() {
        if ($this->isRemoved()) {
            $this->restore();
        } else {
            $this->remove();
        }
    }

    /**
     * Detects if current DBObject represents not existed DB record.
     *
     * @return bool
     */
    public function isNewRecord() {
        if (is_null(static::ID_FIELD_NAME)) {
            return true;
        }

        return ($this->id == 0);
    }

    /**
     * Saves DBObject to the database. If this is a new object - INSERT SQL
     *           instruction executes, if existed one - UPDATE.
     *
     * @param bool $debug Debug mode flag.
     *
     * @return mixed Primary key value.
     * @throws DBCoreException If some database error occurred.
     */
    public function save($debug = false) {
        if ($this->isNewRecord()) {
            $insertionId = DBCore::insertDBObject($this, false, $debug);
            if (Tools::isInteger($insertionId) && $insertionId > 0) {
                $this->setId($insertionId);

                return $insertionId;
            }
            throw new DBCoreException("Save database object error");
        }
        DBCore::updateDBObject($this, $debug);

        return $this->id;
    }

    /**
     * Inserts DBObject to the database.
     *
     * @param bool $ignore Ignore unique indexes or not.
     * @param bool Debug mode flag.
     *
     * @return mixed Primary key value.
     * @throws DBCoreException If some database error occurred.
     */
    public function insert($ignore = false, $debug = false) {
        return DBCore::insertDBObject($this, $ignore, $debug);
    }

    /**
     * Inits SQL query.
     *
     * @param string $queryType Type of the SQL query from types list from DBQuery.
     * @param array $conditions List of conditions for WHERE instruction.
     * @param array $fields List of fields for INSERT or UPDATE types of SQL queries.
     *
     * @return DBObject Oneself.
     * @throws DBCoreException If some error occurred.
     */
    public function initQuery($queryType, $conditions = [], $fields = []) {
        $this->dbQuery = new DBPreparedQuery();

        $this->dbQuery->setType($queryType);

        if (!is_array($conditions)) {
            throw new DBCoreException("Invalid conditions array");
        }
        $this->dbQuery->conditions = $conditions;

        if (!is_array($fields)) {
            throw new DBCoreException("Invalid fields array");
        }
        $this->dbQuery->fields = $fields;

        /*
         * Inits LIMIT if called dynamic select() or update() method.
         */
        if (is_null($this->dbQuery->limit)) {
            $backTrace = debug_backtrace();
            if (is_array($backTrace) && isset($backTrace[1])) {
                $prevCall = $backTrace[1];
                if (is_array($prevCall) && isset($prevCall['type'])) {
                    if ($prevCall['type'] == '->') { // dynamic method was called
                        $this->dbQuery->limit = 1;
                    }
                }
            }
            unset($backTrace);
        }

        return $this;
    }

    /**
     * Prepare DBObject for the SELECT SQL query.
     *
     * @param array $conditions List of the conditions fields
     *           (fieldName => fieldValue or sqlCondition => params).
     *
     * @return DBObject Current object.
     */
    public function select($conditions = []) {
        return $this->initQuery(DBQueryType::SELECT, $conditions);
    }

    /**
     * Static way to prepare DBObject for the SELECT SQL query.
     *
     * @param array $conditions List of the conditions fields
     *           (fieldName => fieldValue or sqlCondition => params).
     *
     * @return DBObject Current object.
     */
    public static function _select($conditions = []) {
        $ref = new \ReflectionClass(get_called_class());
        $dbObject = $ref->newInstance();

        return $dbObject->initQuery(DBQueryType::SELECT, $conditions);
    }

    /**
     * Select and returns DB record for current DBObject table by record ID.
     *
     * @param mixed $recordId Record ID.
     * @param bool $debug Debug mode flag.
     *
     * @return DBObject Record object or null.
     */
    public static function _get($recordId, $debug = false) {
        return static::_select([
            static::ID_FIELD_NAME => $recordId
        ])->limit(1)->go($debug);
    }

    /**
     * Returns result of the COUNT() SQL query.
     *
     * @param array $conditions Conditions list.
     * @param type $debug Debug mode flag.
     *
     * @return int
     */
    public static function _count($conditions = [], $debug = false) {
        $dbQuery = (new DBPreparedQuery())->prepare(
            "SELECT COUNT(*) as 'val' FROM " . static::TABLE_NAME,
            $conditions
        );

        if (!$debug) {
            return (int)DBCore::selectSingleValue($dbQuery);
        }
        $dbQuery->debug();
    }

    /**
     * Returns result of the MAX($field) SQL query.
     *
     * @param string $field Name of the field.
     * @param array $conditions Conditions list.
     * @param type $debug Debug mode flag.
     *
     * @return int
     */
    public static function _max($field, $conditions = [], $debug = false) {
        $dbQuery = (new DBPreparedQuery())->prepare(
            "SELECT MAX(`" . $field . "`) as 'val' FROM " . static::TABLE_NAME,
            $conditions
        );

        if (!$debug) {
            return DBCore::selectSingleValue($dbQuery);
        }
        $dbQuery->debug();
    }

    /**
     * Returns result of the MIN($field) SQL query.
     *
     * @param string $field Name of the field.
     * @param array $conditions Conditions list.
     * @param type $debug Debug mode flag.
     *
     * @return int
     */
    public static function _min($field, $conditions = [], $debug = false) {
        $dbQuery = (new DBPreparedQuery())->prepare(
            "SELECT MIN(`" . $field . "`) as 'val' FROM " . static::TABLE_NAME,
            $conditions
        );

        if (!$debug) {
            return DBCore::selectSingleValue($dbQuery);
        }
        $dbQuery->debug();
    }

    /**
     * Prepare DBObject for the UPDATE SQL query.
     *
     * @param type $fields List of fields to be updated
     *           (fieldName => fieldValue or sqlAssignment => params).
     * @param array $conditions List of the conditions fields
     *           (fieldName => fieldValue or sqlCondition => params).
     *
     * @return DBObject Current object.
     */
    public function update($fields = [], $conditions = []) {
        return $this->initQuery(DBQueryType::UPDATE, $conditions, $fields);
    }

    /**
     * Static way to prepare DBObject for the UPDATE SQL query.
     *
     * @param type $fields List of fields to be updated
     *           (fieldName => fieldValue or sqlAssignment => params).
     * @param array $conditions List of the conditions fields
     *           (fieldName => fieldValue or sqlCondition => params).
     *
     * @return DBObject Current object.
     */
    public static function _update($fields = [], $conditions = []) {
        $ref = new \ReflectionClass(get_called_class());
        $dbObject = $ref->newInstance();

        return $dbObject->initQuery(DBQueryType::UPDATE, $conditions, $fields);
    }

    /**
     * Prepare DBObject for the select query (for ORDER expression).
     *
     * @param array $order List of order conditions (fieldName => order),
     *           order may be 'ASC' OR 'DESC'.
     *
     * @param array $order
     * @return DBObject Current object.
     */
    public function order($order = null) {
        $this->dbQuery->order = $order;

        return $this;
    }

    /**
     * Prepare DBObject for the select query (for LIMIT expression).
     *
     * @param int $offset Limit offset value (or count if this is single
     *           parameter).
     * @param int $count Number of records to select.
     *
     * @return DBObject Current object.
     */
    public function limit($offset = 1, $count = null) {
        if (is_null($offset)) {
            return $this;
        }

        if (is_null($count)) {
            $this->dbQuery->limit = $offset;
        } else {
            $this->dbQuery->limit = [$offset, $count];
        }

        return $this;
    }

    /**
     * Selects DB record(s) for current DBObject table according to params.
     *
     * @param bool $debug Debug mode flag.
     *
     * @return mixed DBObject, array of DBObject or null.
     * @throws DBCoreException If some DB or query syntax errors occurred.
     */
    public function go($debug = false) {
        switch ($this->dbQuery->getType()) {
            case (DBQueryType::SELECT):
                $this->dbQuery->query = "SELECT * FROM " . static::TABLE_NAME;
                break;
            case (DBQueryType::UPDATE):
                $this->dbQuery->query = "UPDATE " . static::TABLE_NAME . " SET ";
                $this->dbQuery->sqlPushValues($this->dbQuery->fields);
                break;
            case (DBQueryType::DELETE):
                $this->dbQuery->query = "DELETE FROM " . static::TABLE_NAME;
                break;
        }

        /*
         * Conditions
         */
        if ($this->isNewRecord()) {
            $this->dbQuery->prepareConditions();
        } else {
            $this->dbQuery->query.= " WHERE ";
            $this->dbQuery->sqlPushValues([static::ID_FIELD_NAME => $this->id]);
        }

        /*
         * Order
         */
        if ($this->isNewRecord()) {
            $this->dbQuery->prepareOrder();
        }

        /*
         * Limit
         */
        $count = null;
        if ($this->isNewRecord()) {
            $count = $this->dbQuery->prepareLimit();
        } else {
            $this->dbQuery->query.= " LIMIT 1";
            $count = 1;
        }

        if ($debug) {
            $this->dbQuery->debug();
        } else {
            if ($this->dbQuery->isSelector()) {
                $stmt = $this->dbQuery->go();
                if ($stmt !== false) {
                    $data = null;
                    if ($count !== 1) {
                        $data = DBCore::selectDBObjectsFromStatement($stmt, $this);
                    } else {
                        $data = DBCore::selectDBObjectFromStatement($stmt, $this);
                    }
                    $stmt->close();

                    return $data;
                }

                return null;
            }

            return $this->dbQuery->go();
        }

        return null;
    }

    /**
     * Deletes DB record for current DBObject.
     *
     * @return mixed Number of affected rows (1 if some record was deleted,
     *            0 - if no) or FALSE if some error occurred.
     */
    public function delete() {
        return DBCore::deleteDBObject($this);
    }

    /**
     * Deletes DB record by ID or condition.
     *
     * @param mixed $conditions List of the conditions fields
     *           (fieldName => fieldValue or sqlCondition => params).
     *           or ID value of the record
     * @return DBObject Current object.
     */
    public static function _delete($conditions = []) {
        $ref = new \ReflectionClass(get_called_class());
        $dbObject = $ref->newInstance();

        if (!is_array($conditions)) { // Just record ID provided
            $recordId = $conditions;
            $conditions = [
                $dbObject->getIdFieldName() => $recordId
            ];
            $dbObject->initQuery(DBQueryType::DELETE, $conditions);
            $dbObject->dbQuery->limit = 1;

            return $dbObject;
        }

        return $dbObject->initQuery(DBQueryType::DELETE, $conditions);
    }

    /**
     * Returns DB table field name by it's camelcase variant.
     *
     * @param string $methodNameFragment
     *
     * @return string
     */
    protected function getFieldName($methodNameFragment) {
        return substr(strtolower(preg_replace("#([A-Z]{1})#", "_$1", $methodNameFragment)), 1);
    }

}