j4nk3y: PHP Oop Datenbank Klasse

Einen wunderschönen guten Abend zusammen,

Da ich seit einiger Zeit im Frontend Objekt orientiert Arbeite und mein PHP seit langem nicht mehr angefasst habe, habe ich mich entschlossen mein vorhandenes Backend auf meinen derzeitigen Kenntnisstand zu aktualisieren.

Nach einiger Recherche und einigen Beispielen habe ich dieses Beispiel einer Datenbank Klasse gefunden.

Wie die URL schon sagt ist es ein einfaches Beispiel, welches selbst meiner Meinung nach sowohl die Vorteile von Prepared Statements vollkommen übergeht als auch logisch einen Nachteil hat, da für jede Query eine neues mysqli-Objekt erstellt wird und somit eine eigene Datenbankverbindung für jede Query.

Dennoch habe ich aus dem Beispiel einiges für mich extrahieren können was ich noch nicht wusste.

Nun habe ich mindestens noch eine Fragen.

  1. call_user_func_array( array( $stmt, 'bind_param'), $foo);, wie muss $foo generell aufgebaut sein? Aus dem Beispiel sieht es für mich so aus, als ob es etwa so aussehen müsste:
    $foo = ['sdi', "colA" => "Foo", "colB" => 13.37, "colC" => 42]. Wäre das so Korrekt?

Weitere Fallen mir gerade nicht ein, denn wenn meine Vermutung bei 1. richtig ist, erübrigen sich meine Fragen die ich Gestern noch hatte.

Schon einmal Danke im voraus und noch einen schönen Abend.

Gruß
Jo

  1. Tach!

    Nach einiger Recherche und einigen Beispielen habe ich dieses Beispiel einer Datenbank Klasse gefunden.

    Lass das lieber. Das ist 4 Jahre alt. Nimm PDO, das ist auch objektorientiert und im Lieferumfang von PHP enthalten, und somit mit jedem Update PHPs aktuell.

    Dennoch habe ich aus dem Beispiel einiges für mich extrahieren können was ich noch nicht wusste.

    Nun habe ich mindestens noch eine Fragen.

    1. call_user_func_array( array( $stmt, 'bind_param'), $foo);, wie muss $foo generell aufgebaut sein? Aus dem Beispiel sieht es für mich so aus, als ob es etwa so aussehen müsste:
      $foo = ['sdi', "colA" => "Foo", "colB" => 13.37, "colC" => 42]. Wäre das so Korrekt?

    Da fängt es schon an, unschön zu werden. Bei einem der zwei Bindings (Parameter oder Ergebnisse, habs vergessen welches der beiden) muss man noch extra Referenzen aus den Array-Elementen machen. Mit PDO sind solche Verrenkungen nicht notwendig.

    Jedenfalls will call_user_func_array() nur ein einfaches Array und kein assoziatives.

    dedlfix.

    1. Hallo dedlfix,

      es ist call_user_func_array total egal ob das Array assoziativ ist oder nicht, es interessiert sich nicht dafür (eben in PHP Sandbox getestet). PHP speichert bekanntlich die Reihenfolge, in der Werte in ein Array gekommen sind, und nur die zählt. Selbst numerische Indexierung wird nicht für eine Reihenfolge beachtet.

      <?php
      function test($a, $b, $c) {
          echo "a=$a, b=$b, c=$c\n";
      }
      
      $ar = [];
      $ar[2] = "1";
      $ar[1] = "22";
      $ar[0] = "333";
      
      call_user_func_array("test", $ar);
      // Ausgabe: a=1, b=22, c=333
      

      Und mysqli braucht, soweit ich das in der php Doku gerade gesehen habe, für Parameter UND Ergebnisse Referenzen auf Variablen. PDO kennt ein bind_value, aber mysqli nicht.

      Rolf

      --
      sumpsi - posui - clusi
      1. Tach!

        es ist call_user_func_array total egal ob das Array assoziativ ist oder nicht, es interessiert sich nicht dafür (eben in PHP Sandbox getestet).

        Na gut, das Handbuch schrub "indexed array". Unabhängig davon, dass es auch mit assoziativen Arrays geht, würde ich das aber trotzdem nicht so verwenden, weil es eher verwirrt, wenn da Elementnamen stehen, die aber gar nicht verwendet werden.

        PHP speichert bekanntlich die Reihenfolge, in der Werte in ein Array gekommen sind, und nur die zählt. Selbst numerische Indexierung wird nicht für eine Reihenfolge beachtet.

        Wenn sie foreach und nicht for mit i nehmen.

        Und mysqli braucht, soweit ich das in der php Doku gerade gesehen habe, für Parameter UND Ergebnisse Referenzen auf Variablen. PDO kennt ein bind_value, aber mysqli nicht.

        Eben, deswegen PDO und nicht mysqli. Das kann sogar ganz ohne Binding einfach Values im Execute übergeben bekommen.

        dedlfix.

    2. Hey,

      Nach einiger Recherche und einigen Beispielen habe ich dieses Beispiel einer Datenbank Klasse gefunden.

      Lass das lieber. Das ist 4 Jahre alt. Nimm PDO, das ist auch objektorientiert und im Lieferumfang von PHP enthalten, und somit mit jedem Update PHPs aktuell.

      Ja, das hab ich mir im Laufe des gestrigen Tages auch schon durch den Kopf gehen lassen. (Auch wenn es mir schon des öfteren in der Vergangenheit gesagt wurde).

      Bevor ich jetzt ein "schlechtes" Beispiel suche/finde, hättest du einen Link für mich?

      Gruß
      Jo

      1. Tach!

        Bevor ich jetzt ein "schlechtes" Beispiel suche/finde, hättest du einen Link für mich?

        Ja, das PDO-Kapitel im PHP-Handbuch. Das hat genügend Beispiele drin.

        dedlfix.

        1. Hey,

          Bevor ich jetzt ein "schlechtes" Beispiel suche/finde, hättest du einen Link für mich?

          Ja, das PDO-Kapitel im PHP-Handbuch. Das hat genügend Beispiele drin.

          Danke, aber ich finde das Manual zum Anfang immer etwas unübersichtlich und macht den Start oft etwas schwierig, weil man mit Informationen und Beispielen für oft sehr spezielle Aufgaben fast schon bombardiert wird. Mittlerweile bin ich aber auch dort angekommen.

          Dazu hab ich kurz hier reingeschaut und etwas entdeckt was mir etwas Kopfschmerzen bereitet. Und zwar ganz unten in der "wichtig!" markierten box.
          Was passiert denn dann mit Fließkommazahlen wenn meine Spalte vom Typ float/double ist und PDO die nur als String behandelt?

          Gruß
          Jo

          1. Hallo j4nk3y,

            probier's im Zweifelsfall aus, ich hab's noch nicht gebraucht. Aber ich würde befürchten, dass Du die double-Zahl als String formatieren und es MySQL überlassen musst, daraus wieder ein double zu interpretieren. Der MySQL Server weiß ja, dass in die DB eine Fließkommazahl gehört.

            Rolf

            --
            sumpsi - posui - clusi
          2. Tach!

            Ja, das PDO-Kapitel im PHP-Handbuch. Das hat genügend Beispiele drin.

            Danke, aber ich finde das Manual zum Anfang immer etwas unübersichtlich und macht den Start oft etwas schwierig, weil man mit Informationen und Beispielen für oft sehr spezielle Aufgaben fast schon bombardiert wird.

            Wenn du in die Beschreibungen der Methoden schaust, ist das wohl so. Aber es gibt auch ein paar Kapitel bevor die Klasse PDO vorgestellt wird. Die sind eher allgemein gehalten.

            Dazu hab ich kurz hier reingeschaut und etwas entdeckt was mir etwas Kopfschmerzen bereitet. Und zwar ganz unten in der "wichtig!" markierten box.

            Ich hab schon ganz oben Kopfschmerzen gefunden:

            "bindValue - Diese Methode arbeitet praktisch genau so wie bind_param von MySQLi."

            Der Satz stimmt überhaupt nicht.

            "Was das im Detail soll, weiß ich auch noch nicht ganz."

            Hat keine Ahnung, aber erklärt anderen die Welt. - Okay. Geht mir ja grundlegend nicht anders. Aber wenn ich so ein Tutorial schreiben wollte, dann würde ich mich wenigstens mal soweit schlau machen, dass ich so einen Satz nicht schreiben muss.

            Was passiert denn dann mit Fließkommazahlen wenn meine Spalte vom Typ float/double ist und PDO die nur als String behandelt?

            Ich kann da nur vermuten, weil ich mich nicht erinnern kann, Fließkommazahlen im DBMS benötigt zu haben. Meist brauch ich die Präzision von Decimal. Hab ich aber auch noch nicht mit PDO verarbeitet. Passieren kann da eigentlich nichts, wenn man das als String behandelt. Aus der Zahl wird ein Literal erstellt, und das DBMS parst das wieder zu einer Zahl. Das ist auch beim Floats nicht grundlegend anders. Literale für Zahlen unterscheiden sich meist auch nicht zwischen den Systemen.

            Wenn man ein SQL-Statement wie SELECT * FROM table WHERE id=42 schreibt, ist die 42 auch nur ein Literal der Zahl 42, das erst geparst werden muss, bevor es mit den Werten der Spalte id verglichen werden kann. Also, Grund für Kopfschmerzen gibts in dem Fall nicht.

            dedlfix.

  2. Hallo j4nk3y,

    ständiges Neuerstellen eines mysqli Handles ist ein Problem, oder auch nicht. Es gilt bei manchen sogar als good practice, eine DB-Connection wie eine heiße Kartoffel zu behandeln: Schnappen, kurz halten, fallenlassen. Dafür wird aber implizit vorausgesetzt, dass die Datenbankanbindung Connection Pooling betreibt, d.h. nicht jedes new mysqli() erzeugt physisch eine neue Connection, sondern es wird eine existierende Connection aus dem Pool wiederverwendet. Das kannst Du in mysqli aktivieren (siehe hier). Ohne das Connection Pooling würde tatsächlich immer eine neue physische Connection hergestellt, das kostet Zeit.

    Das Array bei call_user_func_array ersetzt die einzelnen Parameter, die call_user_func bekäme. Wichtig ist hier, dass das Array ab der zweiten Position Referenzen auf Variablen enthält (siehe hier, 2. Note). Mir wäre unbekannt, dass die Array-Einträge besondere Schlüssel bräuchten, die Zuordnung zu Parametern erfolgt in der Reihenfolge, wie die Einträge ins Array gekommen sind. Werte einzutragen, wie Du es gemacht hast, funktioniert nicht. Man kann SQL-Parameter nur an Variablen binden. Sinn ist, dass Du das einmal tust und dann pro Aufruf des SQL-Statements die Variablenwerte änderst.

    Aber - vergiss die von Dir verlinkte Klasse für produktive Aufgaben, sie ist schlecht gemacht.

    • Parameterbindung und Statementausführung muss trennbar sein, sonst erfüllt die Binderei nur die Hälfte ihres Zwecks (Zweck 1: Escapen der Werte vermeiden, Zweck 2: Der Server bekommt das Statement nur einmal und muss es nur einmal optimieren, danach kann man es N mal benutzen)
    • Einen Connect zu machen und dann nicht zu disconnecten führt zu einer Menge "vergessener" Datenbank-Handles. Wenn man pro Statement neu connected, dann muss man auch pro Statement disconnecten

    Rolf

    --
    sumpsi - posui - clusi
    1. Hey,

      Aber - vergiss die von Dir verlinkte Klasse für produktive Aufgaben, sie ist schlecht gemacht.

      • Parameterbindung und Statementausführung muss trennbar sein, sonst erfüllt die Binderei nur die Hälfte ihres Zwecks (Zweck 1: Escapen der Werte vermeiden, Zweck 2: Der Server bekommt das Statement nur einmal und muss es nur einmal optimieren, danach kann man es N mal benutzen)
      • Einen Connect zu machen und dann nicht zu disconnecten führt zu einer Menge "vergessener" Datenbank-Handles. Wenn man pro Statement neu connected, dann muss man auch pro Statement disconnecten

      Genau das ist mir beim durchsehen eben auch aufgefallen, weshalb ich von vornherein eine gewisse Skepsis aufrechterhalten konnte und nicht einfach nur kopiere.

      Vielen Dank für deine Hinweise!

      Gruß
      Jo

    2. Tach!

      ständiges Neuerstellen eines mysqli Handles ist ein Problem, oder auch nicht. Es gilt bei manchen sogar als good practice, eine DB-Connection wie eine heiße Kartoffel zu behandeln: Schnappen, kurz halten, fallenlassen. Dafür wird aber implizit vorausgesetzt, dass die Datenbankanbindung Connection Pooling betreibt

      Nach meinem Wissen hieß es von Seiten MySQLs, dass der Verbindungsaufbau so schnell vonstattengeht, dass man ruhig einmal mehr eine Verbindung aufbauen kann. Es sei extra für diese Eigenheiten des Webbetriebs mit Scripts statt dauerlaufender Applikationen ausgelegt.

      dedlfix.

      1. Hallo dedlfix,

        solange es auf localhost läuft, würde ich zustimmen.

        Aber wenn nicht, dann dauern die Roundtrips wohl länger. Oder macht connect gar keinen Roundtrip, sondern initialisiert nur lokal und führt den eigentlichen Connect bei erstbester Gelegenheit durch?

        Rolf

        --
        sumpsi - posui - clusi
        1. Tach!

          Aber wenn nicht, dann dauern die Roundtrips wohl länger. Oder macht connect gar keinen Roundtrip, sondern initialisiert nur lokal und führt den eigentlichen Connect bei erstbester Gelegenheit durch?

          Da gibts schon zwei, drei Roundtrips. Hab ich aber erst als negativ bemerkt, als ich mal eine Datenbank in Fernost befragen musste.

          dedlfix.

  3. Moin,

    Allgemeine Frage Nummer 2.

    function fooBar ( $foo, $bar = ['*'], $foobar=null ){
    	//do something
    }
    
    fooBar ( "foo" ); // I
    fooBar ( "foo", /* *** */ , ["foo" => 'foo'] ); // II
    

    Wie kann ich die Funktion fooBar aufrufen ohne eine 2te Variable mitzugeben, sodass $bar= ['*'] ist und $foobar = ["foo" => 'foo'] (Aufruf II)?

    Gruß
    Jo

    1. Tach!

      Wie kann ich die Funktion fooBar aufrufen ohne eine 2te Variable mitzugeben, [...]?

      Das geht (so) nicht. Du kannst nicht nichts übergeben.

      Aber du kannst mit etwas Logik in der Funktion ein übergebenes null auswerten und dann daraufhin einen Defaultwert setzen.

      dedlfix.

      1. Moin,

        Das geht (so) nicht. Du kannst nicht nichts übergeben.

        Aber du kannst mit etwas Logik in der Funktion ein übergebenes null auswerten und dann daraufhin einen Defaultwert setzen.

        Danke schön.

        Sprich:

        function fooBar ( $foo, $bar = null, $foobar=null ){
        	if(!$bar){$bar = ['*']}
        }
        
        

        Gruß
        Jo

        1. Hallo j4nk3y,

          Vorsicht vor dem Type Juggler. Seine Bälle sind explosiv. Es gibt etliche Werte, die PHP als FALSE ansieht und durch Negation dann in TRUE verwandelt. Siehe hier - Converting to booleans. Beachte Niederträchtigkeiten wie "", "0" und ARRAY().

          Wenn Du auf null testen willst, dann tu das. Frage is_null($bar) oder $bar === null ab. Achtung, $bar == null wäre falsch, weil 0 == null wahr ist (wie auch false == null, [] == null und "" == null). Ausnahme ist "0", das ist zwar falsy, aber es ist "0" != null.

          Rolf

          --
          sumpsi - posui - clusi
  4. Wie die URL schon sagt ist es ein einfaches Beispiel, welches selbst meiner Meinung nach sowohl die Vorteile von Prepared Statements vollkommen übergeht als auch logisch einen Nachteil hat, da für jede Query eine neues mysqli-Objekt erstellt wird und somit eine eigene Datenbankverbindung für jede Query.

    Mach doch die Verbindung, egalob mysqli oder PDO, zu einer Eigenschaft der Instanz dann steht sie in jeder Methode zur Verfügung. MfG

    PS: Falls Du auf PDO umsteigst, ändert sich ggf. der Syntax in den einzelnen Methoden. Du könntest ja auch die vorliegende Klasse als Basisklasse nehmen und die Methoden einfach nach Deinen Bedürfnissen überschreiben.

  5. Guten Morgen zusammen,

    Ich hatte gestern schonmal ein wenig Zeit mich an einen ersten Entwurf zu setzen.

    Mir ist bewusst, dass es sicher noch Verbesserungspotenzial gibt aber seht selbst. Für Verbessunrgsvorschläge bin ich gerne offen.

    class DB {
    	
    	private function __construct($user, $password, $database, $host = 'localhost') {
    		self::$user = $user;
    		self::$password = $password;
    		self::$database = $database;
    		self::$host = $host;
    	}
    	
    	private function __clone() {}
    	
    	public static function getInstance(  ) {
    		if ( !self::$instance ) { 
    			self::$instance = new PDO("mysql:dbname=$this->$database;host=$this->$host", $this->$user, $this->$password);
    			self::$instance -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    			self::$instance -> beginTransaction();
    		}
    		
    		return self::$instance;
    	}
    	
    	public function select ( $table, $columns, $where, $limit ) {
    		
    		$stmt = self::$instance -> prepare( $this->prepareQuery ( 'select', $table, $columns, $where, $limit ) );
    		
    		if ( is_array( $where[0] ) ) {
    			$result = [];
    			foreach ( $where as $value ) {
    				$stmt -> bindValues ( $value );
    				if ( !$stmt -> execute() ) {
    					return false;
    				}
    				$result[] =  $stmt -> fetchAll();
    			}
    			return $result
    		} else {
    			$stmt -> bindValues ( $where );
    			if ( !$stmt -> execute() ) {
    				return false;
    			}
    			return $stmt -> fetchAll();
    		}
    	}
    	
    	public function insert ( $table, $data ) {
    		$stmt  = self::$instance -> prepare( $this->prepareQuery ( 'insert', $table, $data  ) );
    		
    		if ( is_array( $data[0] ) ) {
    			foreach ( $data as $value ) {
    				$stmt -> bindValues ( $value );
    				if ( !$stmt -> execute() ) {
    					return false;
    				}
    			}
    			return $result
    		} else {
    			$stmt -> bindValues ( $data );
    			if ( !$stmt -> execute() ) {
    				return false;
    			}
    		}
    		return true;
    	}
    	
    	private function getLastInsertId () {
            return self::$instance->lastInsertId();
        }
    	
    	public function update ( $table, $data, $where ) {
    		$stmt  = self::$instance -> prepare( $this->prepareQuery ( 'update', $table, $data, $where ) );
    		
    		if ( is_array( $data[0] ) ) {
    			foreach ( $data as $key => $value ) {
    				$i = 0;
    				foreach ( $value as $val ) {
    					$toBind[$i] = $val;
    					++$i;
    				}
    				foreach ( $where as $val ) {
    					$toBind[$i] = $val;
    					++$i;
    				}
    				
    				$stmt -> bindValues ( $toBind );
    				
    				if ( !$stmt -> execute() ) {
    					return false;
    				}
    			}
    			return $result
    		} else {
    			$stmt -> bindValues ( $data );
    			if ( !$stmt -> execute() ) {
    				return false;
    			}
    		}
    		return true;
    	}
    	
    	public function delete ( $table, $where ) {
    		$stmt = self::$instance -> query( $this->prepareQuery ( 'delete', $table, $where ) );
    		
    		if ( !$stmt -> execute() ) {
    			
    		}
    	}
    	
    	public function commit () {
    		self::$instance -> commit();
    	}
    	
    	private function rollBack () {
    		self::$instance -> rollBack();
    	}
    	
    	private function prepareQuery ( $type, $table, $data, $where = null, $limit = null ) {
    		
    		$query = '';
    		$columns = '';
    		$placeholders = '';
    		$values = [];
    		
    		if ( !is_null($data) && $type === 'select' ) {
    			$data = ['*'];
    		}
    		
    		if ( $data ) {
    			foreach ( $data as $column => $value ) {
    				
    				if ( $type === 'Select' ) {
    					$columns .= "{$value},";
    				} elseif ( $type === 'insert' ) {
    					$columns .= "{$column},";
    					$placeholders .= '?,';
    				} elseif ( $type === 'update' ) {
    					$columns .= "{$column}=?,";
    				}
    				
    				$values[] = $value;
    			}
    			
    			substr($columns, 0, -1);
    			substr($placeholders, 0, -1);
    			
    			if ( $type === 'Select' ) {
    				$query = "SELECT {$columns} FROM {$table}";
    			} elseif ( $type === 'insert' ) {
    				$query = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
    			} elseif ( $type === 'update' ) {
    				$query = "UPDATE {$table} SET {$columns}";
    				
    			} elseif ( $type === 'delete' ) {
    				$query = "DELETE FROM {$table}";
    			}
    			
    		} else {
    			return false;
    		}
    		
    		if ( $where ) {
    			
    			$query .= " WHERE ";
    			foreach ( $where as $column => $value) {
    				$clause .= "{$column} =? AND ";
    			}
    			
    			substr($clause, 0, -5);
    			$query .= $clause;
    		}
    		
    		if ( $limit ) {
    			$query .= " LIMIT = {$limit}";
    		}
    		
    		return $query;
    	}
    	
    	private function bindValues ( $data ) {
    		$i = 1;
            foreach ( $data as $value ) {
                $varType = is_null( $value ) ? \PDO::PARAM_NULL : is_bool( $value ) ? \PDO::PARAM_BOOL : is_int( $value ) ? \PDO::PARAM_INT : \PDO::PARAM_STR;
    			
                if ( !self::$instance->bindValue ( ++$i, $value, $varType ) ) {
    			   return false;
    			}
            }
            return true;
        }
    	
    	
    	public function commit (  ) {
    		if ( !self::$instance -> commit() ) {
    		   return false;
    		   
    		}
    		return true;
    	}
    	
    	private function rollBack (  ) {
    		if ( !self::$instance -> rollBack() ) {
    		   return false;
    		   
    		}
    		return true;
    	}
    	
    }
    

    Ziel ist es für mich, die Querys in der folgenden Form zu übergeben:

    select ( "tablename",
    				["colA","colB"] || null,
    				["colA" => 1, "colB" => 2] || [["colA" => 1, "colB" => 2], ["colA" => 2, "colB" => 3]],
    				10 || null );
    insert ( "tablename",
    				["colA" => 1, "colB" => 2] || [["colA" => 1, "colB" => 2], ["colA" => 2, "colB" => 3]],
    				["colA" => 2, "colB" => 3] );
    update ( "tablename",
    				["colA" => 1, "colB" => 2] || [["colA" => 1, "colB" => 2], ["colA" => 2, "colB" => 3]],
    				["colA" => 2, "colB" => 3] );
    delete ( "tablename",
    				["colA" => 2, "colB" => 3] );
    

    Dann noch einmal kurz auf die Sicherheit. Escapen bzw die übergebenen Daten behandeln damit eine Injection nicht möglich ist, brauchte ich bei PDO nicht mehr, richtig?

    Und noch ein kleiner Punkt, wie ist das, wenn ich noch keine Datenbank erstellt habe? Reicht es dann mysql:dbname=$this->$database;host=$this->$host zu mysql:dbname=$this->$database zu ändern? Und wie verbinde ich mich dann zu der Datenbank wenn ich diese durch einen CREATE DATABASE-Query erstellt habe?

    Vielen Dank und noch ein schönes Wochenende.

    Gruß
    Jo

    1. Tach!

      Mir ist bewusst, dass es sicher noch Verbesserungspotenzial gibt aber seht selbst.

      bindValues() brauchst du nicht. Da du sowieso nur Einzelstatements abarbeiten kannst und nicht einmal Preparieren und mehrfach Binden und Ausführen, kannst du die Parameter als Array dem execute() übergeben.

      prepareQuery() würde ich separieren für die einzelnen Typen. Lediglich die Where-Klauselerzeugung ist allen gleich, die kann in eine eigene Methode. So wie du es jetzt hast, lassen sich ungültige Statements erzeugen (INSERT mit LIMIT). UPDATE kann zwar auch LIMIT, aber das nimmt man da wohl eher nicht. Statt Schleife zum Zusammenstellen der Feld-/Parameterliste mit anschließendem Entfernen überflüssiger Verkettungsoperatoren kannst du implode() nehmen.

      Dann noch einmal kurz auf die Sicherheit. Escapen bzw die übergebenen Daten behandeln damit eine Injection nicht möglich ist, brauchte ich bei PDO nicht mehr, richtig?

      Wenn du das Prinzip verstanden hast, beantwortet sich diese Frage von selbst. Warum muss man denn maskieren? Weil man Bezeichner und Werte in die Query einfügt und dabei die Begrenzungszeichen beachten muss, damit eine gültige Syntax entsteht und nicht ein unmaskiertes Begrenzungszeichen in einem Bezeichner oder Wert die Query kaputtmacht. Bei Prepared Statements geht die Query separat auf Reisen. Die Werte werden in einem zweiten Schritt verarbeitet. Dabei übergibst du die Werte in Rohform, also nicht wie im Statement als Literal (Stringdarstellung der Werte). Für den sicheren Transport sorgt die API. Deshalb ist da kein Maskieren notwendig. Du musst aber sehr wohl maskieren, wenn du Bezeichner einfügst. Die sind vom Prepared-Statement-Mechanismus nicht betroffen. Auch Stellen wie das LIMIT sind weiterhin zu beachten.

      dedlfix.

      1. Hey,

        Danke für deine Hinweise.

        Ein wenig denke ich konnte ich schon umsetzen, hier nur kurz die einzelnen Methoden:

        public function select ( $columns, $tablel, $where, $limit = null ) {
        	
        	if ( !is_null( $columns ) ) {
        		$columns = ['*'];
        	}
        	
        	$columns = implode(", ", $columns);
        	
        	$query = "SELECT {$columns} FROM {$table}";
        	$query .= $this -> whereClause( $where, $limit );
        	
        	$stmt = self::$instance -> prepare( $query );
        	
        	if ( is_array( $where[0] ) ) {
        		foreach ( $where as $toBind ) {
        			if ( !$stmt -> execute( $toBind ) ) {
        				return false;
        			}
        			$result[] =  $stmt -> fetchAll();
        		}
        	} else {
        		if ( !$stmt -> execute( $where ) ) {
        				return false;
        		}
        		$result =  $stmt -> fetchAll();
        	}
        	return $result;
        }
        
        public function insert ( $table, $data ) {
        	
        	$columns = implode( ", ", array_keys( is_array( $data[0] ) ? $data[0] : $data ) );
        	$placeholders = substr ( str_repeat("?, ", count( is_array( $data[0] ) ? $data[0] : $data ) )) , 0, -2 );
        	
        	$query = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
        	
        	$stmt  = self::$instance -> prepare( $query ) );
        	
        	if ( is_array( $data[0] ) ) {
        		foreach ( $data as $toBind ) {
        			if ( !$stmt -> execute( $toBind ) ) {
        				return false;
        			}
        		}
        	} else {
        		if ( !$stmt -> execute( $toBind ) ) {
        			return false;
        		}
        	}
        	return true;
        }
        
        public function update ( $table, $data, $where ) {
        	
        	$columns = implode( "=?, ", array_keys( is_array( $data[0] ) ? $data[0] : $data ) );
        	
        	$query = "UPDATE {$table} SET {$columns}";
        	$query .= $this -> whereClause( $where );
        	
        	$stmt  = self::$instance -> prepare( $query );
        	
        	if ( is_array( $data[0] ) ) {
        		$i = 0;
        		foreach ( $data as $value ) {
        			foreach ( $value as $val ) {
        				$toBind[$i] = $val;
        				++$i;
        			}
        			foreach ( $where as $val ) {
        				$toBind[$i] = $val;
        				++$i;
        			}
        			if ( !$stmt -> execute( $toBind ) ) {
        				return false;
        			}
        			$i = 0;
        		}
        	} else {
        		foreach ( $data as $val ) {
        			$toBind[$i] = $val;
        			++$i;
        		}
        		foreach ( $where as $val ) {
        			$toBind[$i] = $val;
        			++$i;
        		}
        		if ( !$stmt -> execute( $toBind ) ) {
        			return false;
        		}
        	}
        	
        	return true;
        }
        
        public function delete ( $table, $where ) {
        	
        	$query = "DELETE FROM {$table}";
        	
        	$query .= $this -> whereClause( $where );
        	
        	$stmt = self::$instance -> query( $query );
        	
        	foreach ( $where as $toBind ) {
        		if ( !$stmt -> execute( $toBind ) ) {
        			return false;
        		}
        	}
        	return true;
        }
        
        public function query ( $query ) {
        	if (! self::$instance->exec( $query ) ) {
        		return false
        	}
        	return true;
        }
        
        private function whereClause ( $where, $limit ) {
        	if ( !is_null( $where ) ) {
        		$query .= " WHERE ";
        		foreach ( $where as $column => $value) {
        			$clause .= "{$column} =? AND ";
        		}
        		
        		substr($clause, 0, -5);
        		$query .= $clause;
        	}
        	
        	if ( !is_null( $limit ) ) {
        		$query .= " LIMIT = {$limit}";
        	}
        	
        	return $query;
        }
        

        Gruß
        Jo

        1. Guten morgen zusammen,

          Habe da noch eine Frage, da ich die Antwort nicht finden kann.

          $stmt -> execute( $toBind );
          

          Kann das Array $toBind Schlüssel enthalten? Sprich $toBind = ["colA" => 1] oder sollten es nur die Werte mit den Standard Schlüsseln der Form: $toBind = array_values(["colA" => 1])?

          Gruß
          Jo

          1. Tach!

            Habe da noch eine Frage, da ich die Antwort nicht finden kann.

            Die steht im PHP-Handbuch, Beispiele 2 und 3.

            dedlfix.

    2. Moin,

      ERRMODE::EXCEPTION ist auf jeden Fall eine gute Idee. Zwei Anmerkungen zur Namensgebung:

      1. Bedenke, dass PDO ein austauschbarer Layer ist, der also neben MySQL auch andere Engines unterstützt. Wenn Du also nur mit MySQL arbeitest und nie vorhast eine andere Engine einzusetzen, wäre der Name DB für Deine Klasse ok. Kommen jedoch weitere Layer (Sybase, Oracle..) ins Spiel, kann das anhand des Klassennamen nicht unterschieden werden. Meine Idee wären Subklassen, DB::MySQL, DB::Oracle usw.

      2. Der Name der Methode getInstance() ist irreführend weil ja der Konstruktor die Methode ist, welche die Instanz erstellt. Deine Methode getInstance() erstellt ja keine Instanz Deiner Klasse sondern stellt eine DB-Sitzung her, also eine Instanz der Klasse PDO.

      Wenn man von PDO erben könnte (was ich nicht beurteilen kann) wären für Deine Klassen auch Namen wie PDO::MySQL, PDO::Sybase usw. denkbar. Ich würde mir das mal angucken. Auf jeden Fall ist eine solche Nomenklatur, was den Aufbau von Klassenhierarchien betrifft, eine Sache die sich auch in Perl bewährt hat, siehe CPAN.

      Ansonsten führt ERRMODE::EXCEPTION zwangsläufig zu einer auf Exceptions basierenden Fehlerbehandlung, Stichwort Exception Chaining. D.h., daß Exceptions bis zur Anwendung Deiner Klasse duchgereicht werden und der Anwender das in Sachen Fehlerbehandlung berücksichtigen muss. Z.B. indem er jeden Methodenaufruf zwingend in einen try/catch Block setzen muss.

      PS: Die Übergabe des Ports würde ich auf jeden Fall noch reinnehmen und den Default auf 3306 setzen. Ein Default für den Host jedoch würde ich nicht setzen, das ist ein systematischer Fehler weil Du nicht gleich merkst, wenn Du auf dem falschen Host gelandest bist.

      1. Tach!

        ERRMODE::EXCEPTION ist auf jeden Fall eine gute Idee.

        PDO::ERRMODE_EXCEPTION

        Meine Idee wären Subklassen, DB::MySQL, DB::Oracle usw.

        Themengebiet ist PHP. Bei der Schreibweise X::Y steht X für eine Klasse, Y für ein Mitglied dieser Klasse. Klassen können in PHP keine Mitglieder anderer Klassen sein. Wenn man kennzeichen möchte, dass DB und MySQL zusammenhängen, kann man das über Namspaces wie in DB\MySQL oder über eine Namenskonvention à la DB_MySQL tun. (Der Unterstrich hat keine syntaktische Bedeutung, ist nur ein anders als die Buchstaben aussehendes Zeichen.)

        Wenn man von PDO erben könnte (was ich nicht beurteilen kann) wären für Deine Klassen auch Namen wie PDO::MySQL, PDO::Sybase usw. denkbar.

        Nicht in der Syntax. Und ja, man kann von PDO erben, es ist nicht als final gekennzeichnet.

        Ich würde mir das mal angucken. Auf jeden Fall ist eine solche Nomenklatur, was den Aufbau von Klassenhierarchien betrifft, eine Sache die sich auch in Perl bewährt hat, siehe CPAN.

        Da hat es den Sinn, eine große Bibliothek zu erstellen, die viele verschiedene Themengebiete abdeckt. Das kann man auch in seinen eigenen Projekten so handhaben, aber das lohnt sich erst dann wirklich, wenn das Projekt ein bisschen grüßer geworden ist. Namespaces wären in PHP das Mittel der Wahl für eine hierarchiche Ordnung.

        Ansonsten führt ERRMODE::EXCEPTION zwangsläufig zu einer auf Exceptions basierenden Fehlerbehandlung, Stichwort Exception Chaining. D.h., daß Exceptions bis zur Anwendung Deiner Klasse duchgereicht werden und der Anwender das in Sachen Fehlerbehandlung berücksichtigen muss.

        "Ein allgemeiner Fehler ist aufgetreten." Da weiß man ja sofort, was die Ursache ist. - Exception chaining kann man machen, finde ich aber absolut unprickelnd, wenn man nur eine generische Exception bekomt und erstmal durch einen Rattenschwanz innerer Exceptions mit nichtssagenden Meldungstexten durchhangeln muss, um an die eigentliche Ursache zu kommen. Wenn man weiter oben sowieso nur eine generelle Exception abfangen kann, dann fängt catch (Exception $e) auch die spezielle.

        dedlfix.

        1. Tach!

          ERRMODE::EXCEPTION ist auf jeden Fall eine gute Idee.

          PDO::ERRMODE_EXCEPTION

          Meine Idee wären Subklassen, DB::MySQL, DB::Oracle usw.

          Themengebiet ist PHP. Bei der Schreibweise X::Y steht X für eine Klasse, Y für ein Mitglied dieser Klasse. Klassen können in PHP keine Mitglieder anderer Klassen sein. Wenn man kennzeichen möchte, dass DB und MySQL zusammenhängen, kann man das über Namspaces wie in DB\MySQL oder über eine Namenskonvention à la DB_MySQL tun.

          Ja klar. In meinen PHP-Klassenhierarchien habe ich mich auch für den Unterstrich entschieden.

          Und ja, man kann von PDO erben, es ist nicht als final gekennzeichnet.

          Gut. Klassenentwurf beginnt mit der am weitesten unten liegenden Anwenderklasse (nach E.F. Johnson), z.B. mit PDO_MySQL_WebLog wobei die Klasse WebLog sämtliche Statements in Methoden kapselt, die für eine Log-Auswertung gebraucht werden. Der Anwender einer solchen Klasse hantiert also nicht mit Statements sondern mit Methoden, z.B.

          $dbh->showlog(url => '/index.html', from => '1.1.2017', to => '12.10.2017');
          

          was freilich auch mit PHP geht nur mit einem anderen Syntax. Das Ergebnis wird direkt an die Templating Engine übergeben die damit eine Tabelle in den Browser rendert.

          Er kann jedoch auch eigene Statements in seiner Anwendung platzieren weil ja PDP_MySQL_WebLog von PDO_MySQL erbt und die DBSession somit eine public Eigenschaft in der eigenen Instanz ist.

          So jedenfalls sehe ich die Ziele von DB-Klassen, stets offen für Erweiterungen. MfG

  6. Noch eine Überlegung zum Sinn und Zweck einer solchen Klasse. Man könnte eine Universal-Methode vorhalten z.B. für Insert-Statements. So wäre der Name der Tabelle das erste zu übergebende Argument und die weiteren Argumente liegen in einem assoziativen Array nach dem Schema colname=>value

    In der Methode wird dann kumulative ein Pool gebildet, in welchem sich je Tabellenname die Statements befinden. D.h., bei jedem Aufruf der Methode wird anhand des übergebenen Tabellennamen das entsprechende Statement aus dem Pool gefischt oder es wird zunächst neu erstellt bevor es ausgeführ wird.

    Gesammelte Statements setzen natürlich voraus, daß Verbindung nicht jedesmal neu aufgebaut werden muss. Nun kannste Dir ungefähr vorstellen wie eine solche Instanz aufgebaut sein muss. Der Rest ist Tipparbeit. Aber Du wirst eine dauerhafte Freude daran haben, für Insert Statements einfach nur noch eine Funktion aufrufen zu müssen, die auch in einer Schleife notiert sein kann:

    $pdo->insert('address', array(name => 'Fuß', vname => 'Konrad'));
    

    MfG

    1. Tach!

      Gesammelte Statements setzen natürlich voraus, daß Verbindung nicht jedesmal neu aufgebaut werden muss.

      Wird es aber bei PHP. Der Aufwand lohnt sich nicht für die drei Statements die im Script abgesetzt werden, bevor das PHP-Script ans Ende kommt, damit der Cache für die Statements verschwindet (wenn es lediglich eine PHP-Variable ist) und die Verbindung zur Datenbank getrennt wird.

      dedlfix.

      1. Mit der Wiederverwendung von Prepared Statements meine ich nicht das Cachen der Verbindung. Der Vorteil der sich aus der Wiederverwendung von Prepared Statements ergibt, ist ja auch in der Doku zu PDO beschrieben Siehe auch.

        Wenn man jedoch eine Methode hat, die bei jeden Aufruf das Statement jedesmal neu prepared, dann wird o.g. Vorteil nicht genutzt. Also wird man ein bestimmtes Statement nur einmal preparieren und solange speichern wie die Instanz am Leben ist.

        Einen solchen Speicher können wir auch als Cache bezeichnen und natürlich funktioniert ein Solcher nur solange wie auch eine Verbindung vorhanden ist. In einer Universal Methode wird man also anhand der übergebenen Argumente feststellen, ob es dafür bereits ein prepared Statement gibt, es verwenden können oder vor der ersten Verwendung erstellen müssen.

        MfG

        1. Tach!

          Mit der Wiederverwendung von Prepared Statements meine ich nicht das Cachen der Verbindung. Der Vorteil der sich aus der Wiederverwendung von Prepared Statements ergibt, ist ja auch in der Doku zu PDO beschrieben Siehe auch.

          Soweit so klar und ein alter Hut.

          Wenn man jedoch eine Methode hat, die bei jeden Aufruf das Statement jedesmal neu prepared, dann wird o.g. Vorteil nicht genutzt. Also wird man ein bestimmtes Statement nur einmal preparieren und solange speichern wie die Instanz am Leben ist.

          Die Instanz ist nicht länger als eine Scriptlaufzeit am Leben. Selbst wenn die Verbindung in einem Pool gecached wird, das Prepared Statement wird es nicht. Man müsste also den Anwendungsfall haben, dass das Statement mehrfach im Script genutzt wird. Das ist der Fall, wenn mehrere Datensätze einfügen möchte. Sowas kommt gelegentlich vor und wird sich meistens an genau einer Stelle innerhalb einer Schleife abspielen. Aber recht unwahrscheinlich ist, dass diese Nutzungen desselben Statements unabhängig voneinander von mehreren Stellen des Scripts aus ausgeführt werden sollen.

          Einen solchen Speicher können wir auch als Cache bezeichnen und natürlich funktioniert ein Solcher nur solange wie auch eine Verbindung vorhanden ist.

          Ich halte eine generelle Ausführung ohne konkreten Anlass eines solchen Features für unnötigen Aufwand mit kaum bis keinem Nutzen.

          dedlfix.

          1. Moin,

            Die Instanz ist nicht länger als eine Scriptlaufzeit am Leben.

            Genau.

            Selbst wenn die Verbindung in einem Pool gecached wird, das Prepared Statement wird es nicht.

            Von sich aus nicht.

            Man müsste also den Anwendungsfall haben, dass das Statement mehrfach im Script genutzt wird. Das ist der Fall, wenn mehrere Datensätze einfügen möchte.

            Richtig! Und das heißt bezogen auf den Entwurf einer eigenen Klasse, daß eine diesbezügliche Methode mehrfach aufgerufen wird..

            Sowas kommt gelegentlich vor und wird sich meistens an genau einer Stelle innerhalb einer Schleife abspielen.

            .. und beliebig oft aufgerufen werden kann solange die Instanz noch am Leben ist.

            Aber recht unwahrscheinlich ist, dass diese Nutzungen desselben Statements unabhängig voneinander von mehreren Stellen des Scripts aus ausgeführt werden sollen.

            Das interessiert doch den Anwender nicht. Er ruft einfach nur eine Methode auf und darf das auch an an mehreren Stellen in seiner Anwendung ohne sich darum kümmern zu müssen welche Statements dafür innerhalb dieser Methode präpariert werden. Genau deswegen entwickeln wir ja eine Klasse und dokumentieren die API.

            Einen solchen Speicher können wir auch als Cache bezeichnen und natürlich funktioniert ein Solcher nur solange wie auch eine Verbindung vorhanden ist.

            Ich halte eine generelle Ausführung ohne konkreten Anlass eines solchen Features für unnötigen Aufwand mit kaum bis keinem Nutzen.

            Die Performance steigt um mindestens den Faktor 2 (zwei). Ich hatte mal eine etwas umfangreichere Anwendung so zu optimieren, die war danach zehnmal so schnell. MfG

    2. Noch eine Überlegung zum Sinn und Zweck einer solchen Klasse. Man könnte eine Universal-Methode vorhalten z.B. für Insert-Statements. So wäre der Name der Tabelle das erste zu übergebende Argument und die weiteren Argumente liegen in einem assoziativen Array nach dem Schema colname=>value

      $pdo->insert('address', array(name => 'Fuß', vname => 'Konrad'));
      

      Das war der Sinn warum ich damit angefangen habe. DB->insert() sieht gerade so bei mir aus:

      //DB -> insert ('tablename', 
      	//				["colA" => 1, "colB" => 2, ...] || [["colA" => 1, "colB" => 2], ["colA" => 2, "colB" => 3], ...] )
      	
      	public function insert ( $table, $data ) {
      		
      		$columns = implode( ", ", array_keys( isset($data[0]) ? $data[0] : $data ) );
      		$placeholders = substr ( str_repeat("?, ", count( isset($data[0]) ? $data[0] : $data ) ) , 0, -2 );
      		
      		$query = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
      		
      		$stmt  = self::$instance -> prepare( $query );
      		
      		if ( isset($data[0]) ) {
      			foreach ( $data as $toBind ) {
      				if ( !$stmt -> execute( array_values( $toBind ) ) ) {
      					return false;
      				}
      			}
      		} else {
      			if ( !$stmt -> execute( array_values( $data ) ) ) {
      				return false;
      			}
      		}
      		return true;
      	}
      

      Logisch schwieriger finde ich es bei einem Select-Statement und Update-Statements. Nehmen wir an es gibt einen Wald in meiner Datenbank. Dieser Wald besteht aus Bäumen, welche Äste besitzen an denen Blätter hängen.

      Nun geht ein Wanderer immer den gleichen Weg durch den Wald und kennt jeden Baum am Rand seines Weges.

      Objekt orientiert würde ich jetzt für jeden Baum, den er kennt, einzeln ein Objekt erzeugen welches die Daten über sein Äste aus der Datenbank holt, um dann für jeden Ast, neue Blätter Objekte zu erzeugen welche die Daten wieder einzeln aus der Datenbank holen.

      Das nutzt nach meinem Verständnis aber nicht die Vorteile von Prepared Statements. (siehe dazu).

      Wohingegen procedual, ich einfach 3 Statments vorbereiten kann (nämlich Select Baum, Select Ast, Select Blatt) und für den entsprechenden Wald ich in geschachtelten Schleifen alle Statments ausführen kann um dann ein Array mit allen Daten habe.

      Zudem muss ich mir jetzt überlegen ob ich unterschiedliche Klassen für einen Wald mache, einmal zum erzeugen des Waldes und einmal zum bearbeiten des Waldes (Bäume wachsen, Blättern fallen im Herbst, Wanderer entscheiden sich einen neuen Weg zu gehen).

      Gruß
      Jo

      1. Mahlzeit;

        für die Implementierung einer Insert-Universal-Methode gibt es zumindest in Perl 2 Möglichkeiten

        1. über prepared Statements
        2. direktes insert

        Bei beiden wären Tabellenname und col=>val zu übergeben, bei (1) zusätzlich und optional eine sog. finish Anweisung. Stell Dir vor, Du feuerst eine Reihe von Inserts über eine Schleife, erst hier kommen ja prepared Statements erst richtig zum Tragen. Also wäre das Statement zu cachen, das kannst Du über static machen oder in der Instanz. Ob (2) in PHP überhaupt möglich ist, kann ich nicht beurteilen. Auf jeden Fall ist es unnötig, für ein einmaliges Insert ein extra Statement zu präpariern, weil es ja nur einmal gebraucht wird. Das setzt natürlich voraus, daß eine Methode zum Quoten vorhanden ist, in Perl gibt es 2 Methoden, quote_identifier und quote zur Vermeidung von Injektions.

        Zudem muss ich mir jetzt überlegen ob ich unterschiedliche Klassen für einen Wald mache, einmal zum erzeugen des Waldes und einmal zum bearbeiten des Waldes (Bäume wachsen, Blättern fallen im Herbst, Wanderer entscheiden sich einen neuen Weg zu gehen).

        Genau. Hier kommen Klassenerweiterungen ins Spiel. Ich hab das immer so gemacht, daß jede qualifizierte Subklasse eine Methode create hat womit für sämtliche Tabellen die Create-Statements verewigt sind. Damit sind sie auch dokumentiert. Bspw. definiert meine Klasse MyDBI_Log in der create-Methode das Create-Statement zum Erstellen der Tabelle log.

        Eine Methode truncate() hingegen kann wiederum in der Basisklasse definiert sein, da wird ja eh nur der Tabellenname übergeben. Weitere Kandidaten für Universalmethoden wären describe, show tables und show create und evntl. auch die Möglichkeit zur freien Übergabe von SQL Anweisungen. Mit der richtigen Klasse in der Hand kannst Du das ganze DB-Management auf der Kommandozeile abfackeln ohne mit der Maus in phpmyadmin rumzufummeln.

        Und falls Deine gesammelten Werke einer Versionskontrolle unterliegen könntest Du auch über eine Metohde alter() nachdenken. Dann landen auch die gesammelten Änderungen am DB Design im Repository. Nicht vergessen, dann auch die create()-Methode anzupassen.

        MfG