Ein Blick auf DB Abstraction - PHP / MySql

729
edorian

Es ist ein bisschen mehr Code, aber ich wollte die volle Klasse zeigen. Ich hebe die Punkte hervor, die ich nach der Quelle eingeben möchte.

Ich habe Kommentare geschnitten, da sie nicht in Englisch waren und die wichtigen übersetzt haben.

Die Klasse ist inspiriert von PearDb (zu alt) und Zend_DB (zu dieser Zeit unübersichtlich) und wird in einer Inhouse-Anwendung verwendet. Ich weiß, es ist keine Idee, einen eigenen Datenbank-Handler zu schreiben (ich würde so weit sagen, ziemlich sinnlos :)), aber angesichts dessen lässt sich der Code betrachten:

Code

<?php

class DbStatement {

    private $oStatement;
    private $aFieldnames = array();
    private $aResultRow = array();
    private $aResultSet = array();
    private $bMetadata = true;
    private $bPreparedFetch = false;
    private $iNumRows = false;
    private $sQuery;
    private $aArgs;
    private $fQuerytime;

    /**
     * @throws DbException
     * @param mysqli $oDb Datenbankverbindung
     * @param string $sQuery
     */
    public function __construct(mysqli $oDb, $sQuery) {
        $this->oStatement = $oDb->prepare($sQuery);
        $this->sQuery = $sQuery;
        if($this->oStatement === false) {
            switch($oDb->errno) {
                case 1054:
                    throw new DbNoSuchFieldException($oDb->error, $oDb->errno);
                case 1146:
                    throw new DbNoSuchTableException($oDb->error, $oDb->errno);
                default:
                    throw new DbException(
                        "Prepared Statement could not be created: ".
                        $oDb->error." (".$oDb->errno."). Query was: '$sQuery'",
                        $oDb->errno
                    );
            }
        }
    }

    /**#@+
     *
     * @param mixed $mParams,...
     */

    public function execute() {
        $this->_execute(func_get_args());
        $this->_done();
    }

    /**
     * @return null|bool|int|string|float
     */
    public function getOne() {
        $this->_execute(func_get_args());
        $this->_fetchRow();
        if(isset($this->aResultSet[0][$this->aFieldnames[0]])) {
            return $this->aResultSet[0][$this->aFieldnames[0]];
        }
        return null;
    }

    /**
     * @return array
     */
    public function getCol() {
        $this->_execute(func_get_args());
        $this->_fetchAll();
        $sIndex = $this->aFieldnames[0];
        $aReturn = array();
        foreach($this->aResultSet as $aResultRow) {
            $aReturn[] = $aResultRow[$sIndex];
        }
        return $aReturn;
    }

    /**
     * @return array
     */
    public function getRow() {
        $this->_execute(func_get_args());
        $this->_fetchRow();
        if(isset($this->aResultSet[0])) {
            return $this->aResultSet[0];
        }
        return array();
    }

    /**
     * @return array
     */
    public function getAssoc() {
        $this->_execute(func_get_args());
        $this->_fetchAll();
        if(isset($this->aFieldnames[0]) && isset($this->aFieldnames[1])) {
            $sIndexKey = $this->aFieldnames[0];
            $sIndexValue = $this->aFieldnames[1];
            $aReturn = array();
            foreach($this->aResultSet as $aResultRow) {
                $aReturn[$aResultRow[$sIndexKey]] = $aResultRow[$sIndexValue];
            }
            return $aReturn;
        }
        return array();
    }

    /**
     * @return array
     */
    public function getAll() {
        $this->_execute(func_get_args());
        $this->_fetchAll();
        return $this->aResultSet;
    }
    /**#@-*/

    /**
     * @return false|int
     */
    public function numRows() {
        return $this->iNumRows;
    }

    /**
     * @return int
     */
    public function affectedRows() {
        return $this->oStatement->affected_rows;
    }

    /**
     * @return int
     */
    public function lastInsertId() {
        return $this->oStatement->insert_id;
    }

    public function getLastExecutedQuery() {
        $sReturn = $this->sQuery;
        if($this->aArgs) {
            $sReturn .= "; -- Argumente: ~".implode("~,~", $this->aArgs)."~";
        }
        return $sReturn;
    }

    /**
     * @throws DbException
     *
     * @param array $aArgs
     */
    private function _execute($aArgs) {
        $aArgs = $this->_parseFuncArgs($aArgs);
        $this->aArgs = $aArgs;
        $iArgs = count($aArgs);
        if($iArgs) {
            if($this->oStatement->param_count !== $iArgs ) {
                throw new DbException(
                    "Inserting parameters failed: ".$this->oStatement->param_count.
                    " Parameters expected but ".$iArgs." passed."
                );
            }
            $aRefArgs = array();
            foreach(array_keys($aArgs) as $mIndex) {
                $aRefArgs[$mIndex] = &$aArgs[$mIndex];
            }
            array_unshift($aRefArgs, str_repeat("s", $iArgs));
            // Needs References
            call_user_func_array(array($this->oStatement, "bind_param"), $aRefArgs);
        }
        $bWorked = $this->oStatement->execute();
        if($bWorked === false) {
            $sError = sprintf(
                "Query failed: %s (%s) Query was: '%s'",
                $this->oStatement->error,
                $this->oStatement->errno,
                $this->sQuery
            );
            switch($this->oStatement->errno) {
                case 1062:
                    throw new DbKeyViolationException($sError, $this->oStatement->errno);
                default:
                    throw new DbException($sError, $this->oStatement->errno);
            }

        }

        $this->_prepareFetch();
    }

    private function _prepareFetch() {
        if($this->bMetadata && !$this->bPreparedFetch) {
            $oMeta = $this->oStatement->result_metadata();
            if($oMeta === false) {
                $this->bMetadata = false;
            } else {
                $this->_prepareMetadata($oMeta);

                $this->aResultRow = array_fill(0, count($this->aFieldnames), null);
                // Ugly but 'bind_result' forces you to pass references
                $aRefs = array();
                foreach ($this->aResultRow as $iIndex => &$rmValue) {
                    $aRefs[$iIndex] = &$rmValue;
                }

                call_user_func_array(array($this->oStatement, "bind_result"), $this->aResultRow);
                $this->bPreparedFetch = true;
            }
        }
    }

    /**
     * @param mysqli_result $oMeta
     */
    private function _prepareMetadata(mysqli_result $oMeta) {
        $aFields = $oMeta->fetch_fields();
        foreach($aFields as $oField) {
            $this->aFieldnames[] = $oField->name;
        }
    }

    private function _fetchRow() {
        $this->_fetch(true);
    }

    private function _fetchAll() {
        $this->_fetch(false);
    }

    /*
     * @param bool $bOne One line ?
     */
    private function _fetch($bOne) {
        $this->aResultSet = array();
        if($bOne !== true) {
            $this->oStatement->store_result();
        }
        while($this->oStatement->fetch()) {

            // Deref
            $aTmp = array();
            foreach($this->aResultRow as $mValue) {
                $aTmp[] = $mValue;
            }
            $this->aResultSet[] = array_combine($this->aFieldnames, $aTmp);

            if($bOne === true) {
                break;
            }
        }

        $this->iNumRows = $this->oStatement->num_rows;
        $this->_done();
    }

    private function _done() {
        $this->oStatement->free_result();
    }

    /**
     * @param array $aArgs
     * @return array
     */
    private function _parseFuncArgs($aArgs) {
        if(isset($aArgs[0]) && is_array($aArgs[0])) {
            return $aArgs[0];
        }
        return $aArgs;
    }

}

Die hässlichen Teile (ich denke, deshalb sind wir hier, denke ich :)) sind drin _executeund _prepareFetchals die MySqli-API uns zwang, Referenzen zu verwenden.

Die Klasse setzt voraus, dass das in mysqli übergebene Objekt bereits mit einer Datenbank verbunden ist.

Wenn ich etwas verpasst habe, lass es mich wissen.

Antworten
18
Dies ist eines der lesbarsten Bits von PHP, das ich je gesehen habe, sollte mit den ursprünglichen Kommentaren noch besser sein. Vielleicht könnten Sie die allgemeine Massage vor `call_user_func_array` in` _execute` und `_prepareFetch` in einen Helfer extrahieren? Hmm, es nicht wert, gut genug, wie es ist. TryPyPy vor 10 Jahren 3
Dies ist ein fantastischer Code. Ich kann wirklich nicht viel sehen, wo man es optimieren kann. Eine mögliche Ergänzung, die Sie hinzufügen könnten, ist die Möglichkeit, die Klasse auch zum Erstellen Ihrer Abfrage zu verwenden. So etwas wie die CodeIgniter-Methoden von $ db-> select ('id, name') -> from (... ` Hailwood vor 10 Jahren 0
Ein Tipp zur Verbesserung von Code ist das direkte Schreiben von Kommentaren in Englisch. Dies wird in Zukunft viele Probleme ersparen. rightfold vor 9 Jahren 0
@Time Machine, die nicht so gut funktioniert, wenn nicht jeder in Ihrem Team Englisch versteht (seltsam, ich weiß, aber wir arbeiten daran) Danke :) edorian vor 9 Jahren 0

2 Antworten auf die Frage

14
TryPyPy

Vielleicht könnten Sie die allgemeine Massage vor call_user_func_array in _executeund _prepareFetchin einen Helfer extrahieren ?

private function _execute($aArgs) {
/// [...]
        $aRefArgs = array();
        foreach(array_keys($aArgs) as $mIndex) {
            $aRefArgs[$mIndex] = &$aArgs[$mIndex];
        }
        array_unshift($aRefArgs, str_repeat("s", $iArgs));
        // Needs References
        call_user_func_array(array($this->oStatement, "bind_param"), $aRefArgs);
///-^ This part...
/// [...]

private function _prepareFetch() {
/// [...]
            $this->aResultRow = array_fill(0, count($this->aFieldnames), null);
            // Ugly but 'bind_result' forces you to pass references
            $aRefs = array();
            foreach ($this->aResultRow as $iIndex => &$rmValue) {
                $aRefs[$iIndex] = &$rmValue;
            }

            call_user_func_array(array($this->oStatement, "bind_result"), $this->aResultRow);
///-^ ... matches this one loosely. 
/// [...]

Wahrscheinlich nicht wert, gut genug, wie es ist.

Dies ist eines der lesbarsten Bits von PHP, das ich je gesehen habe, sollte mit den ursprünglichen Kommentaren noch besser sein.

7
bitsoflogic

Obwohl ich die privaten Methoden nicht genau geprüft habe, wollte ich einige Rückmeldungen zu verschiedenen öffentlichen Methoden geben.

$dbStatement = new dbStatement($mysqliDb, 'SELECT * FROM users');
echo $dbStatement->numRows(); // false
$users = $dbStatement->getAll(); // Users array
echo $dbStatement->numRows(); // Some int
  1. Es scheint einige Inkonsistenzen zu geben, wie einige Methoden funktionieren. Einige Methoden lösen die Ausführung der Anweisung aus, andere dagegen nicht. Daher glaube ich, dass dies zu unerwartetem Verhalten führen wird. Siehe oben für Codebeispiel.

  2. Sie haben vielleicht einen guten Grund dafür, aber ich bin auch neugierig, warum numRows () standardmäßig auf false gesetzt ist, während betroffeneRows () und lastInsertId () dies nicht sind.

  3. Ich habe auch bemerkt, dass getLastExecutedQuery () die an die Klasse übergebene Abfrage zurückgibt, unabhängig davon, ob sie ausgeführt wurde oder nicht. Sie wird im Konstruktor vorbereitet, aber erst mit der privaten Methode _execute () ausgeführt.

Die Klasse setzt voraus, dass das in mysqli übergebene Objekt bereits mit einer Datenbank verbunden ist.

Schließlich würde ich empfehlen, sicherzustellen, dass die Klasse eine Ausnahme auslöst oder dies auf andere Weise zurecht macht, wenn das mysqli-Objekt nicht mit einer Datenbank verbunden ist.

Es ist schon einige Monate her, seit ich die Frage gestellt habe, aber trotzdem: Vielen Dank für das Feedback! Punkt 1) und 2) berühren einen Punkt, den ich nie in Betracht gezogen oder getestet habe. Es scheint auch, dass ich nie falsch eingesetzt wurde. Wirklich toll, dass du das erwischt hast, es sollte zumindest so berichten, wie ich denke :) - 3) wurde falsch benannt und vor einiger Zeit entfernt. ---- Das Problem "Nicht verbunden" wird von der Methode "Ausführen" etwas korrekt behandelt, da beim Ausführen der Abfrage eine Ausnahme "Nicht verbunden" ausgelöst wird. Vielen Dank für das Rückgespräch! edorian vor 9 Jahren 0