borisbaer: PDO: Dynamische Read- und Write-Funktion schreiben

Hallo zusammen,

in meiner Model-Class möchte ich Funktionen unterbringen, um mit der mySQL-Datenbank zu interagieren. Ich habe bereits eine read-Funktion geschrieben, mit der ich prinzipiell zufrieden bin:

public static function read( $table, $column, $condition = [] )
{
	$attrs = array_keys( $condition );
	$sql = implode( 'AND', array_map( fn( $attr ) => "$attr = :$attr", $attrs ) );

	if ( !$condition ) {
		$stmt = self::prepare( "SELECT $column FROM $table" );
	} else {
		$stmt = self::prepare( "SELECT $column FROM $table WHERE $sql" );
		foreach ( $condition as $key => $value ) {
			$stmt -> bindValue( ":$key", $value );
		}
	}

	$stmt -> execute();

	$results = $stmt -> fetchAll( PDO::FETCH_ASSOC );
	return $results;
}

Ein entsprechender Aufruf wäre zum Beispiel: $releases = Model::read( 'releases', 'id, value, game', [ 'game' => $page ] );

Ich habe versucht, eine ähnliche Funktion für das Einfügen in die Datenbank zu schreiben …

public function dbWrite( $table, $condition, $values )
{
	$attrs = array_values( $condition );
	$condition = implode( ',', $condition );
	$sql = implode( ',', array_map( fn( $attr ) => ":$attr", $attrs ) );
	$stmt = self::prepare( "INSERT INTO $table ( $condition ) VALUES ( $sql )" );
	foreach ( $values as $key => $value ) {
		$stmt -> bindValue ( ":$key", $value );
	}

	$stmt -> execute();
}

… bin jedoch nicht wirklich zufrieden damit, da ich das Gefühl habe, es könnte besser gehen.

Um zum Beispiel einen neuen Benutzer zur Datenbank hinzuzufügen, könnte man schreiben: Model::dbWrite( 'users', [ 'username', 'email', 'password' ], [ 'username' => $username, 'email' => $email, 'password' => $password ] );

Dies scheint mir ein wenig zu viel des Guten in Bezug auf die Länge des Befehls. Wie könnte man das Ganze eleganter lösen?

Ich wäre sehr dankbar für ein wenig Hilfe.

Grüße
Boris

P.S.: Soweit ich gelesen habe, ist es nicht nötig, filter_input zu verwenden, wenn man mit PDO arbeitet, oder?

akzeptierte Antworten

  1. Hallo borisbaer,

    Model::read?

    Nö - sicherlich hast Du doch auch eine Release-Klasse für die Release-Objekte, oder? Und eine User-Klasse für die User-Objekte.

    Hast Du nicht? Na dann, ran! Diese Klassen müssen Propertys enthalten, die den DB-Columns entsprechen (gleiche Namen sind am einfachsten, sonst programmierst Du Dir die Finger mit Mappern wund), und ein paar Methoden, die die Model-Funktionen passend für diesen Objekttyp (äh, also diese Klasse) parametrieren.

    Und dann sieht das nicht mehr so aus:

    $releases = Model::read( 'releases', 'id, value, game', [ 'game' => $page ] );
    

    sondern so:

    $releases = Release::readForGame($page);
    
    class Release {
    ...
       public static function readForGame($gameId)
       {
         Model::read( 'Release', 
                      'releases',
                      'id, value, game', 
                      [ 'game' => $gameId ] );
       }
    }
    
    class Model
    {
       static function read($className, $table, $column, $condition = [] )
       {
          ...
          $results = $stmt -> fetchAll( PDO::FETCH_CLASS, $className );
       }
    }
    

    Guck Dir fetchAll mit FETCH_CLASS (bzw. fetchClass) mal an. Die Klasse Release muss gar nicht viel enthalten, nur die Properties und das Interface zur Datenbank.

    Ach ja, und ein Flag "$isNew" oder so. Das setzt Du auf FALSE, wenn das Objekt aus der Datenbank geladen wurde, und auf TRUE, wenn es neu ist.

    Wenn die fachlichen Propertys genauso heißen wie die DB-Spalten, macht PDO den Rest automatisch. Wenn nicht, wird es mühsamer, darauf gehe ich aber nur dann ein wenn Du das unbedingt wissen willst.

    Jetzt hast Du sicher auch die Idee für das Schreiben verstanden. Implementiere in der Release-Klasse (und in der User-Klasse und in der Game-Klasse und und und) eine Methode save(). Diese soll:

    • abfragen, ob isNew gesetzt ist. Wenn ja, muss ein INSERT gemacht werden, sonst ein UPDATE.
    • für INSERT ein Model::dbCreate aufrufen mit passenden Angaben für Spaltennamen und Werte
    • für UPDATE ein Model::dbUpdate aufrufen mit passenden Angaben für zu ändernde Spalten, deren Werte und einer Condition um das richtige Element zu erwischen

    Diese Funktionsaufrufe können gerne riesig sein. Das ist das Kabelknäuel unter der Arbeitsplatte, da guckt nachher keiner mehr hin.

    In der eigentlichen Anwendung schreibst Du dann nur noch

    $user->save();
    

    und der Rest passiert unter der Haube.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Ach ja, und ein Flag "$isNew" oder so.

      Naja. War da nicht eine ID, die in der Regel ein Autoinkrement der Datenbank ist?

      Ist die NULL (nicht: 0), dann ist das doch ausreichend „flaggy“

      1. Hallo Raketenwilli,

        ja ok, kann man so machen. Wenn man Autoinkrement-IDs für jede Table verwendet. Das ist in meinen DBs öfter mal nicht der Fall, die sind oftmals anders geschlüsselt und haben die ID ihrer Parent-Row zusammen mit einem weiteren Attribut als Key. Die muss ich dann auch beim Neuanlegen vorbelegen, sonst kriege ich die Assoziation nicht hin.

        Einen solchen zusammengesetzten Schlüssel als Clustering Key zu verwenden hat durchaus Performance-Vorteile, das spart dem Kopf der Festplatte im DB-Server ggf. einige Meter Flugweg. Wenn X die ID der Parent-Row ist und der composite key das Tupel (X, Z) ist, dann sind alle abhängigen Rows zum parent key X auf der Festplatte beisammen, wenn (X,Z) der Clustering Key ist und lassen sich schnell lesen. Wenn X und Z nur Fremdschlüssel sind, es eine eigene ID gibt und ich einen unique index auf (X,Z) lege, kann es durchaus passieren, dass die benötigten Rows für X kreuz und quer in der Tablespace-Datei verteilt sind.

        Und ja, diese Performancegewinne sind für mich relevant und spürbar.

        Rolf

        --
        sumpsi - posui - obstruxi
    2. Hallo Rolf,

      […] sicherlich hast Du doch auch eine Release-Klasse für die Release-Objekte, oder? Und eine User-Klasse für die User-Objekte.

      ich habe bis jetzt nur eine Controller-Klasse für die Releases, die wiederum zwei Methoden hat – eine für eine GET-Anfrage und eine für eine POST. In der Methode für die GET-Anfrage befindet sich bspw. der Befehl Model::read( […] ). Wenn ich dich richtig verstanden habe, dann soll ich noch eine Model-Class für die Releases-Objekte erstellen?

      Hast Du nicht? Na dann, ran! Diese Klassen müssen Propertys enthalten, die den DB-Columns entsprechen (gleiche Namen sind am einfachsten, sonst programmierst Du Dir die Finger mit Mappern wund), und ein paar Methoden, die die Model-Funktionen passend für diesen Objekttyp (äh, also diese Klasse) parametrieren.

      Das sollte kein Problem sein. In der Releases.model-Klasse müssen also nur Properties instantiiert (sagt man das so?) werden und schon weiß die PDO-Methode, in welche Spalten die übertragenen Daten sollen?

      Guck Dir fetchAll mit FETCH_CLASS (bzw. fetchClass) mal an. Die Klasse Release muss gar nicht viel enthalten, nur die Properties und das Interface zur Datenbank.

      Interface zur Datenbank? Das ist mir neu. Was meinst du damit?

      Ich habe im Ordner App/Models die Klasse Release erstellt und die sieht folgendermaßen aus:

      class Release {
      
      	public int $id;
      	public int $value;
      	public string $game = '';
      	
      	public static function readForGame( $page )
      	{
      		Model::dbRead( 'Release', 'releases', 'id, value, game', [ 'game' => $page ] );
      	}
      
      }
      

      Den Rest habe ich entsprechend deinem Vorschlag geändert, zum Beispiel dass jetzt die Ergebnisse über fetchAll( PDO::FETCH_CLASS, $class ) abgegriffen werden.

      Jedoch erhalte ich die Fehlermeldung: Fatal error: Class "Release" not found in D:\Websites\framework\app\models\Model.php on line 53

      Die entsprechende Code-Zeile ist genau die:

      $results = $stmt -> fetchAll( PDO::FETCH_CLASS, $class );
      

      Ach ja, und ein Flag "$isNew" oder so. Das setzt Du auf FALSE, wenn das Objekt aus der Datenbank geladen wurde, und auf TRUE, wenn es neu ist.

      Ja, das mache ich, sobald ich hoffentlich das Prinzip und die Umsetzung hinter deinem Vorschlag verstanden habe. 😅

      Wenn die fachlichen Propertys genauso heißen wie die DB-Spalten, macht PDO den Rest automatisch. Wenn nicht, wird es mühsamer, darauf gehe ich aber nur dann ein wenn Du das unbedingt wissen willst.

      Das muss nicht sein. Die Properties werden wie die Spalten heißen.

      Jetzt hast Du sicher auch die Idee für das Schreiben verstanden. Implementiere in der Release-Klasse (und in der User-Klasse und in der Game-Klasse und und und) eine Methode save(). Diese soll:

      • abfragen, ob isNew gesetzt ist. Wenn ja, muss ein INSERT gemacht werden, sonst ein UPDATE.
      • für INSERT ein Model::dbCreate aufrufen mit passenden Angaben für Spaltennamen und Werte
      • für UPDATE ein Model::dbUpdate aufrufen mit passenden Angaben für zu ändernde Spalten, deren Werte und einer Condition um das richtige Element zu erwischen

      Siehe oben. Das gehe ich an, sobald die Read-Methode funktioniert.

      Viele Grüße
      Boris

      1. Hallo borisbaer,

        Interface zur Datenbank? Das ist mir neu. Was meinst du damit?

        Das, worüber wir hier reden. Die Methoden an der Modell-Klasse (also nicht Model, sondern eine Klasse des fachlichen Modells wie Release), die zur Kommunikation mit der DB dienen und damit die Spezifika der jeweiligen Klasse einkapseln.

        Fatal error: Class "Release" not found in D:\Websites\framework\app\models\Model.php on line 53

        Verwendest Du Namespaces? Steckt die Release-Klasse in einem solchen? Dann müsstest Du PDO den Klassennamen incl. Namespace übergeben.

        Das Thema $isNew hat Raketenwilli ja schon in Frage gestellt. Denk also erstmal über das nach, was er schrieb, bevor Du das einbaust.

        ich habe bis jetzt nur eine Controller-Klasse für die Releases, die wiederum zwei Methoden hat – eine für eine GET-Anfrage und eine für eine POST. In der Methode für die GET-Anfrage befindet sich bspw. der Befehl Model::read( […] ).

        Ja, genau - und das ist zuviel auf einmal. Der Controller behandelt Benutzeraktionen und enthält die nötige Logik, um die Benutzeraktion umzusetzen. Das ist schon eine Menge Zeug, er sollte sich nicht auch noch mit der Abbildung der DB auf fachliche Objekte beschäftigen. Sowas können die auch selbst.

        Das hat den Vorteil, dass Du diesen Kram nicht mehrfach programmieren musst. Sicherlich wirst Du in unterschiedlichen Situationen Releases oder auch User laden müssen, und dann ist es sinnvoll, wenn der DB-Krimskrams dafür an einer Stelle ist.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Die Methoden an der Modell-Klasse (also nicht Model, sondern eine Klasse des fachlichen Modells wie Release), die zur Kommunikation mit der DB dienen und damit die Spezifika der jeweiligen Klasse einkapseln.

          Ach so, ich dachte, du meinst das hier. Begriffsverwirrung herrscht bei mir irgendwie häufig in Bezug auf das MVC-Pattern. Ich weiß nicht, ob ich es richtig verstanden habe vom Prinzip her. Wegen der Komplexität habe ich es mal versucht grafisch darzustellen:

          Es gibt also vier „Schnittstellen“:

          • das Master-Model (\Core\Model) interagiert mit der DB durch die sogenannten CRUD-Methoden (möglichst dynamisch).
          • Die „fachlichen“ Models (z.B. \App\Models\Releases) übergeben die wichtigen Parameter durch Properties.
          • die „fachlichen“ Controllers (z.B. \App\Controllers\Releases) überprüfen den User-Input und bestimmen, was je nach HTTP-Request-Method passieren soll
          • die view-page zeigt die vom Controller bereitgestellten Daten an

          Ist das vom Prinzip her so gemeint gewesen?

          Mir erschließt sich aber leider noch nicht, wie irgendwelche definierten Properties im fachlichen Model für die PDO-Anfrage gebraucht werden können. 😵

          Fatal error: Class "Release" not found in D:\Websites\framework\app\models\Model.php on line 53

          Verwendest Du Namespaces? Steckt die Release-Klasse in einem solchen? Dann müsstest Du PDO den Klassennamen incl. Namespace übergeben.

          Ja, war tatsächlich so. Jetzt erscheint diese Fehlermeldung nicht mehr.

          Das Thema $isNew hat Raketenwilli ja schon in Frage gestellt. Denk also erstmal über das nach, was er schrieb, bevor Du das einbaust.

          Das mache ich.

          1. Hallo borisbaer,

            interessantes Bild - kann man so machen, muss man nicht.

            Diese "Master Model" Klasse, die die CRUD Methoden implementieren soll, scheint mir eher sowas wie eine Toolbox mit den nötigen Methoden für den Model-Betrieb zu sein. Es gibt da diverse Ansätze, einer ist auch der, dass die Release- oder User-Klasse von einer AbstractModel Klasse erbt und Methoden wie dbRead oder dbCreate dort zu finden sind.

            Man kann das beliebig over-engineeren.

            Als mein Arbeitgeber in den 90er Jahren mit Smalltalk rumgemacht hat, war ein "Mapping Tool" Teil des Entwicklungssystems, das wir dafür eingekauft hatten. Da musste man mit einer GUI-Oberfläche das Objektmodell auf das Relationenmodell der DB abbilden. Daraufhin generierte das Mapping-Tool dann das SQL für die Zugriffe automatisch; es konnte sogar JOINs und Subselects aufbauen. Das kostete prompt einige Strafsekunden in der Performance.

            Hinzu kam dann die Architekturanweisung, dass die Datenhaltung nicht in einer Server-DB zu erfolgen habe, sondern auf unserem guten alten IBM Großrechner. Auf den kann man natürlich NICHT mit generiertem SQL zugreifen - d.h. kann man schon, das wird aber aus Performance-Gründen verboten. Heißt also: für jedes potenziell generierte SQL musste man ein COBOL Modul auf dem Host bereitstellen, in dem das SQL dann tatsächlich stand. Und im Smalltalk eine Mapping-Tabelle führen, welches SQL Statement welches COBOL Modul aufzurufen hat. Nachdem wir uns zwei Jahre lang damit die Finger gebrochen haben, habe ich darauf gepfiffen, die Datenzugriffsmodule direkt aufgerufen und die Modellobjekte aus den Rückgabedaten von Hand generiert. Das fluppte wie Seife.

            Hach ja. Die Nostalgie ist auch nicht mehr das, was sie mal war…

            Rolf

            --
            sumpsi - posui - obstruxi
            1. Hallo Rolf,

              interessantes Bild - kann man so machen, muss man nicht.

              ja, das ist ja irgendwie generell das „Problem“ bei MVC für Laien wie mich. Jeder macht es irgendwie anders. Mir schien es so sinnvoll zu sein.

              Diese "Master Model" Klasse, die die CRUD Methoden implementieren soll, scheint mir eher sowas wie eine Toolbox mit den nötigen Methoden für den Model-Betrieb zu sein. Es gibt da diverse Ansätze, einer ist auch der, dass die Release- oder User-Klasse von einer AbstractModel Klasse erbt und Methoden wie dbRead oder dbCreate dort zu finden sind.

              So kann man das betrachten, ja. Ein Werkzeugkasten mit möglichst universellen CRUD-Methods. Prinzipiell könnte man auch in jeder einzelnen (fachlichen) Model-Class speziell auf diese zugeschusterte CRUD-Methods schreiben. Ich weiß nicht, was besser ist.

              Warum hast du eigentlich den Modus bei FetchAll auf PDO::FETCH_CLASS umgestellt? Was bringt das?

              Als mein Arbeitgeber in den 90er Jahren mit Smalltalk rumgemacht hat, war ein "Mapping Tool" Teil des Entwicklungssystems, das wir dafür eingekauft hatten. Da musste man mit einer GUI-Oberfläche das Objektmodell auf das Relationenmodell der DB abbilden. Daraufhin generierte das Mapping-Tool dann das SQL für die Zugriffe automatisch; es konnte sogar JOINs und Subselects aufbauen. Das kostete prompt einige Strafsekunden in der Performance.

              Hinzu kam dann die Architekturanweisung, dass die Datenhaltung nicht in einer Server-DB zu erfolgen habe, sondern auf unserem guten alten IBM Großrechner. Auf den kann man natürlich NICHT mit generiertem SQL zugreifen - d.h. kann man schon, das wird aber aus Performance-Gründen verboten. Heißt also: für jedes potenziell generierte SQL musste man ein COBOL Modul auf dem Host bereitstellen, in dem das SQL dann tatsächlich stand. Und im Smalltalk eine Mapping-Tabelle führen, welches SQL Statement welches COBOL Modul aufzurufen hat. Nachdem wir uns zwei Jahre lang damit die Finger gebrochen haben, habe ich darauf gepfiffen, die Datenzugriffsmodule direkt aufgerufen und die Modellobjekte aus den Rückgabedaten von Hand generiert. Das fluppte wie Seife.

              Leider verstehe ich hier größtenteils nur Bahnhof. 😄 Soll mir die Geschichte etwas sagen?

              Grüße
              Boris

              1. Hallo borisbaer,

                Warum hast du eigentlich den Modus bei FetchAll auf PDO::FETCH_CLASS umgestellt? Was bringt das?

                Lies doch erst mal im PHP Handbuch bevor Du fragst 😟

                Wenn Du eine Klasse 'Release' erstellst, möchtest Du doch, dass PDO Dir Objekte dieser Klasse erzeugt.

                FETCH_CLASS bewirkt, dass automatisch ein Objekt der angegebenen Klasse erstellt wird. Deswegen auch $className als zusätzlicher Parameter. Du hattest FETCH_ASSOC, dann entsteht ein assoziatives Array und man braucht keinen weiteren Parameter.

                Bahnhof. 😄 Soll mir die Geschichte etwas sagen?

                Das sollte eine Story zum Thema "epic fail durch over-engineering" sein. Over Engineering findet statt, wenn man zulässt, dass sich kreative Architekten in einer Software austoben und nicht drüber nachdenken, ob alles, was man tun KANN, auch das ist, was man tun SOLLTE.

                Rolf

                --
                sumpsi - posui - obstruxi
                1. Hallo Rolf,

                  Lies doch erst mal im PHP Handbuch bevor Du fragst 😟

                  meinst du den folgenden Auszug:

                  PDO::FETCH_CLASS: returns a new instance of the requested class, mapping the columns of the result set to named properties in the class, and calling the constructor afterwards, unless PDO::FETCH_PROPS_LATE is also given. If mode includes PDO::FETCH_CLASSTYPE (e.g. PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE) then the name of the class is determined from a value of the first column.

                  Tut mir leid, da bleiben bei mir trotzdem viele Fragen offen. Wo müssen die entsprechenden Properties stehen? Im Constructor? Das müsste mir jemand einfacher erklären.

                  Wenn Du eine Klasse 'Release' erstellst, möchtest Du doch, dass PDO Dir Objekte dieser Klasse erzeugt.

                  FETCH_CLASS bewirkt, dass automatisch ein Objekt der angegebenen Klasse erstellt wird. Deswegen auch $className als zusätzlicher Parameter. Du hattest FETCH_ASSOC, dann entsteht ein assoziatives Array und man braucht keinen weiteren Parameter.

                  Wahrscheinlich eignet sich das für mich bei dieser Tabelle gar nicht, weil sie Daten aus zwei Quellen einspeist (JSON-Datei und Datenbank-Tabelle, siehe hier). Aus diesen beiden Quellen merge ich ein Array, dass dann die HTML-Tabelle ausgibt.

                  Das sollte eine Story zum Thema "epic fail durch over-engineering" sein. Over Engineering findet statt, wenn man zulässt, dass sich kreative Architekten in einer Software austoben und nicht drüber nachdenken, ob alles, was man tun KANN, auch das ist, was man tun SOLLTE.

                  Das kann ich als Nicht-ITler selten bzw. nie beurteilen, wann die Grenze zum over-engineering überschritten ist. 🤷‍♂️

                  Grüße
                  Boris

                  1. Hallo borisbaer,

                    Wo müssen die entsprechenden Properties stehen? Im Constructor?

                    Properties sind ein Teil der Klassendefinition. Im Konstruktor kannst Du sie befüllen, wenn Du willst - aber das nimmt Dir PDO ab.

                    class FooObject {
                       public $id;
                       public $name;
                       public $vorname;
                    }
                    
                    $stmt = $db->prepare("SELECT id, name, vorname FROM table WHERE id=:id");
                    $stmt->execute( [ "id" => 7 ] );
                    $stmt->fetchObject("FooObject");
                    

                    Fertig.

                    Rolf

                    --
                    sumpsi - posui - obstruxi
                    1. Hallo Rolf,

                      Properties sind ein Teil der Klassendefinition. Im Konstruktor kannst Du sie befüllen, wenn Du willst - aber das nimmt Dir PDO ab.

                      class FooObject {
                         public $id;
                         public $name;
                         public $vorname;
                      }
                      
                      $stmt = $db->prepare("SELECT id, name, vorname FROM table WHERE id=:id");
                      $stmt->execute( [ "id" => 7 ] );
                      $stmt->fetchObject("FooObject");
                      

                      Fertig.

                      so, jetzt habe ich es verstanden. Sorry, manchmal stehe ich bei neuen Informationen erst mal auf dem Schlauch.

                      Danke!

        2. Ich habe noch das Problem, das durch die veränderte Fetch-Methode die Ausgabe einen anderen Datentyp hat, und zwar:

          $releases = Array
          (
              [0] => App\Models\Release Object
                  (
                      [id] => 3
                      [value] => 1
                      [game] => demons-souls
                  )
          
              [1] => App\Models\Release Object
                  (
                      [id] => 7
                      [value] => 0
                      [game] => demons-souls
                  )
          
              [2] => App\Models\Release Object
                  (
                      [id] => 5
                      [value] => 0
                      [game] => demons-souls
                  )
          
          )
          

          Es handelt sich nicht mehr um Arrays im Array, sondern um Objects. Ich merge aber an anderer Stelle zwei Arrays:

          if ( !empty( $releases ) ) {
          	foreach( $json['td'] as $key => $value ) {
          		$json['td'][$key] = array_merge( $value, [ 'value' => $releases[$key]['value'] ] );
          	}
          }
          

          Das funktioniert jetzt nicht mehr, denn: Error Message: Cannot use object of type App\Models\Release as array. Wie kann ich das reparieren?

  2. Lieber borisbaer,

    wenn Du ein Model hast, ist das dann nicht eine Repräsentation eines Datenobjektes?

    Ein entsprechender Aufruf wäre zum Beispiel: $releases = Model::read( 'releases', 'id, value, game', [ 'game' => $page ] );

    Also für mich sieht das so aus, als sei ein Release ein Objekt. Dieses darf dann auch „selber wissen“, wie man hierfür die DB genau bespielen muss. Das Release-Objekt „kennt“ dann auch die DB-Struktur und kann darauf maßgeschneiderten SQL-Code generieren.

    foreach (Release::get_all() as $r) {
      echo "Game: ", $r->game, PHP_EOL;
      echo "Year: ", $r->date, PHP_EOL;
    }
    
    $n = new Release();
    $n->game = 'The Witcher 3';
    $n->date = '2015-05-18';
    $n->save();
    

    Das, was Du mit id sozusagen von Hand notierst, weil Deine read-Methode für „Handbetrieb“ ausgelegt ist, muss die Release-Klasse selbst intern managen. Dafür kennt sie die Tabellenstruktur.

    P.S.: Soweit ich gelesen habe, ist es nicht nötig, filter_input zu verwenden, wenn man mit PDO arbeitet, oder?

    Eine Geschichte vom Hörensagen... Hast Du Dich erkundigt, was der Sinn hinter filter_input ist und wofür Du es wirklich(!) benötigst?

    Liebe Grüße

    Felix Riesterer

    1. Hallo Felix,

      wenn Du ein Model hast, ist das dann nicht eine Repräsentation eines Datenobjektes?

      leider mangelt es mir an der Fachsprache, um das wirklich beantworten zu können. Ich weiß nur, dass Model-Klassen allein mit der Datenbank „kommunizieren“ und die Ergebnisse dann an entsprechende Controller-Klasse weitergeben.

      Ein entsprechender Aufruf wäre zum Beispiel: $releases = Model::read( 'releases', 'id, value, game', [ 'game' => $page ] );

      Also für mich sieht das so aus, als sei ein Release ein Objekt. Dieses darf dann auch „selber wissen“, wie man hierfür die DB genau bespielen muss. Das Release-Objekt „kennt“ dann auch die DB-Struktur und kann darauf maßgeschneiderten SQL-Code generieren.

      foreach (Release::get_all() as $r) {
        echo "Game: ", $r->game, PHP_EOL;
        echo "Year: ", $r->date, PHP_EOL;
      }
      
      $n = new Release();
      $n->game = 'The Witcher 3';
      $n->date = '2015-05-18';
      $n->save();
      

      Das, was Du mit id sozusagen von Hand notierst, weil Deine read-Methode für „Handbetrieb“ ausgelegt ist, muss die Release-Klasse selbst intern managen. Dafür kennt sie die Tabellenstruktur.

      Meine Lösung sieht so aus: Die HTML-Tabelle generiert sich aus Daten einer JSON-Datei und einer Datenbank-Tabelle. In der JSON-Datei stehen dann z.B. „Titel“, „Erscheinungsjahr“ usw. (also statische Werte) und in der Datenbank-Tabelle wird gemanagt, welchen Wert ein bestimmter input zu dem entsprechenden Release-Eintrag hat („habe ich“, „habe ich nicht“, „kein Interesse“). Diese Vorgehensweise macht es für mich nötig, IDs manuell in der JSON-Datei zu vergeben. Womöglich kann man das besser lösen, doch das übersteigt meine aktuellen Kenntnisse.

      P.S.: Soweit ich gelesen habe, ist es nicht nötig, filter_input zu verwenden, wenn man mit PDO arbeitet, oder?

      Eine Geschichte vom Hörensagen... Hast Du Dich erkundigt, was der Sinn hinter filter_input ist und wofür Du es wirklich(!) benötigst?

      Ich werde es mir bei Gelegenheit durchlesen, danke! Habe mich wohl auch übrigens falsch ausgedrückt (siehe hier).

      1. Lieber borisbaer,

        Du verwendest zwar eine Datenbank, aber nur für Teilaufgaben der Datenhaltung?

        Meine Lösung sieht so aus: Die HTML-Tabelle generiert sich aus Daten einer JSON-Datei und einer Datenbank-Tabelle. In der JSON-Datei stehen dann z.B. „Titel“, „Erscheinungsjahr“ usw. (also statische Werte) und in der Datenbank-Tabelle wird gemanagt, welchen Wert ein bestimmter input zu dem entsprechenden Release-Eintrag hat („habe ich“, „habe ich nicht“, „kein Interesse“).

        Ein Mischmasch von Datenbank und JSON-Dateien. Warum nur willst Du das genau so haben? Der Vorteil beim Einsatz einer Datenbank ist ja gerade, dass man Bezüge herstellen kann. Deswegen sind die Daten aus den JSON-Dateien besser in der DB aufgehoben, natürlich in einer anderen Tabelle.

        Diese Vorgehensweise macht es für mich nötig, IDs manuell in der JSON-Datei zu vergeben.

        Das ist der Beweis dafür, dass Deine Vorgehensweise völliger Quatsch ist. Die ID-Werte soll die Datenbank schön selbst verwalten. Dann klappt das auch mit der Konsistenz der Daten.

        Womöglich kann man das besser lösen, doch das übersteigt meine aktuellen Kenntnisse.

        Genau dafür ist ja das Forum da. Mein Vorschlag:

        Tabelle releases mit den Spalten

        • id (Primärschlüssel, integer, auto_increment)
        • game (varchar 250)
        • date (date)

        Tabelle user

        • login (Primärschlüssel, varchar 250)
        • pw (varchar 200)
        • email (varchar 250)

        Tabelle attitudes

        • id (Primärschlüssel, integer, auto_increment)
        • attitude (varchar 250)

        Tabelle gaming (da hast Du sicherlich einen besseren Namen)

        • id (Primärschlüssel, integer, auto_increment)
        • user (varchar 250) <- bezieht sich auf user.login
        • release (integer) <- bezieht sich auf releases.id
        • interest (integer) <- bezieht sich auf attitudes.id

        Die Tabelle releases soll der Datenstruktur Deiner JSON-Dateien entsprechen. Die Tabelle gaming ist die eigentliche Zuordnung von User, Release und Interesse. Anstatt hier magic numbers für das jeweilige Interesse zu wählen, kann man das auch mit einer passenden Werte-Tabelle (hier attitudes) verknüpfen.

        Mein Beispiel ist sicher unvollständig, aber hoffentlich kann es wenigstens zeigen, wie Deine Daten allesamt in die DB gehören.

        Das Schreiben der passenden Klassen zum Verwalten der jeweiligen Tabellen und ihren Daten ist dann Stoff für eine abgeleitete Diskussion.

        Liebe Grüße

        Felix Riesterer

        1. Hallo Felix,

          Du verwendest zwar eine Datenbank, aber nur für Teilaufgaben der Datenhaltung?

          nun, zunächst einmal hatte ich die Daten über eine JSON-Datei eingepflegt, dann erst hatte ich die Funktion des Ankreuzens einfügen wollen. War für mich dann logisch, das so zu machen, zumal ich gar nicht wusste, dass man in einer Datenbank die Tabellen verknüpfen kann. In meinem Kopf ist das ein statisches Gebilde gewesen (wie eine simple Excel-Tabelle). Ich dachte, PHP müsse da den Rest erledigen. So kam ich zum Mergen.

          Ein Mischmasch von Datenbank und JSON-Dateien. Warum nur willst Du das genau so haben? Der Vorteil beim Einsatz einer Datenbank ist ja gerade, dass man Bezüge herstellen kann. Deswegen sind die Daten aus den JSON-Dateien besser in der DB aufgehoben, natürlich in einer anderen Tabelle.

          Siehe oben.

          Das ist der Beweis dafür, dass Deine Vorgehensweise völliger Quatsch ist. Die ID-Werte soll die Datenbank schön selbst verwalten. Dann klappt das auch mit der Konsistenz der Daten.

          Ein Vorteil für mich bei den JSON-Dateien war, dass ich die Reihenfolge der Einträge selbst festlegen konnte. Wenn es nicht alphabetisch (oder anders) automatisch sortiert werden soll, sondern manuell, müsste ich in der entsprechenden Datenbank wohl auch noch eine Spalte haben, in der dann die Stelle steht, an der der Eintrag stehen soll.

          Wie ist das außerdem mit der Datensicherheit? Wenn die Tabelle defekt ist und gelöscht wird, sind doch alle Daten weg, nicht wahr? Da helfen nur Backups, oder?

          Genau dafür ist ja das Forum da. Mein Vorschlag:

          Tabelle releases mit den Spalten

          • id (Primärschlüssel, integer, auto_increment)
          • game (varchar 250)
          • date (date)

          Tabelle user

          • login (Primärschlüssel, varchar 250)
          • pw (varchar 200)
          • email (varchar 250)

          Tabelle attitudes

          • id (Primärschlüssel, integer, auto_increment)
          • attitude (varchar 250)

          Tabelle gaming (da hast Du sicherlich einen besseren Namen)

          • id (Primärschlüssel, integer, auto_increment)
          • user (varchar 250) <- bezieht sich auf user.login
          • release (integer) <- bezieht sich auf releases.id
          • interest (integer) <- bezieht sich auf attitudes.id

          Die Tabelle releases soll der Datenstruktur Deiner JSON-Dateien entsprechen. Die Tabelle gaming ist die eigentliche Zuordnung von User, Release und Interesse. Anstatt hier magic numbers für das jeweilige Interesse zu wählen, kann man das auch mit einer passenden Werte-Tabelle (hier attitudes) verknüpfen.

          Das klingt alles sinnvoll, aber die Rolle dieser attitudes-Tabelle habe ich leider nicht verstanden. Außerdem müsste ich noch herausfinden, wie das rein technisch in meiner Datenbank zu realisieren wäre.

          Das Schreiben der passenden Klassen zum Verwalten der jeweiligen Tabellen und ihren Daten ist dann Stoff für eine abgeleitete Diskussion.

          Ja, es ist eben immer ein Rabbit Hole. 🐇

          Grüße
          Boris

  3. Hallo borisbaer,

    P.S.: Soweit ich gelesen habe, ist es nicht nötig, filter_input zu verwenden, wenn man mit PDO arbeitet, oder?

    Klingt nach frisch gepresstem Konfusel.

    Du musst für jeden Kontextwechsel die richtigen Werkzeuge einsetzen. filter_input dient zum Prüfen, ob ein Eingabewert das richtige (fachliche) Format hat und kann auch ein paar unerwünschte Zeichen ausfiltern. Aber den Kontextwechsel von Input zu Datenbank machst Du durch mysqli::real_escape_string oder durch PDO::quote, wenn Du Daten ins SQL einsetzt, oder durch Einsatz von prepared statements.

    Viel wichtiger ist es nachher, wenn Du die Daten wieder ausgibst, htmlspecialchars zu verwenden.

    Das Self-Wiki hat zum Kontextwechsel einen Artikel mit zwei Teilen, hier. Der erste Teil ist abstrakter, der zweite Teil (am Ende des ersten verlinkt) geht mehr auf Technik ein.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo

      Viel wichtiger ist es nachher, wenn Du die Daten wieder ausgibst, htmlspecialchars zu verwenden.

      Wenn denn die Ausgabe in HTML erfolgt. Genereller gesagt ist es die zum Ausgabeformat passende Filter- oder Maskierungsfunktion, die zu verwenden ist.

      Das Self-Wiki hat zum Kontextwechsel einen Artikel mit zwei Teilen, hier. Der erste Teil ist abstrakter, der zweite Teil (am Ende des ersten verlinkt) geht mehr auf Technik ein.

      Ich wusste nicht, dass der Artikel mittlerweile aus zwei Teilen besteht. Hab ihn jetzt mal überflogen und bin von der Aufteilung, Ausführlichkeit und den vielen verschiedenartigen Beispielfällen im zweiten Teil angetan. 👍

      Tschö, Auge

      --
      200 ist das neue 35.
    2. Hallo Rolf,

      P.S.: Soweit ich gelesen habe, ist es nicht nötig, filter_input zu verwenden, wenn man mit PDO arbeitet, oder?

      Klingt nach frisch gepresstem Konfusel.

      Du musst für jeden Kontextwechsel die richtigen Werkzeuge einsetzen. filter_input dient zum Prüfen, ob ein Eingabewert das richtige (fachliche) Format hat und kann auch ein paar unerwünschte Zeichen ausfiltern. Aber den Kontextwechsel von Input zu Datenbank machst Du durch mysqli::real_escape_string oder durch PDO::quote, wenn Du Daten ins SQL einsetzt, oder durch Einsatz von prepared statements.

      danke für die Aufklärung. Ich habe mich schlecht ausgedrückt. Ich meinte, ich habe gelesen, man kann auf filter_input verzichten, wenn man die prepare()- bzw. bindValue()-Funktion des PDO-Moduls verwendet.

      Viel wichtiger ist es nachher, wenn Du die Daten wieder ausgibst, htmlspecialchars zu verwenden.

      Ja, das mache ich mittlerweile auch immer.

      Das Self-Wiki hat zum Kontextwechsel einen Artikel mit zwei Teilen, hier. Der erste Teil ist abstrakter, der zweite Teil (am Ende des ersten verlinkt) geht mehr auf Technik ein.

      Ich glaube, ich hatte damals, als du mir den Link geschickt hast, den ersten Teil des Artikels gelesen. Den zweiten allerdings noch nicht. Werde ich auf jeden Fall nachholen, danke!

      1. Ich meinte, ich habe gelesen, man kann auf filter_input verzichten,

        Auf filter_input im Hinblick auf SQL-Injections zu vertrauen ist sogar falsch. Bevorzugt prepare()- bzw. bindValue() oder PDO::quote sind einschlägig.

  4. Aloha,

    ich lese da ein "self::prepare".

    Das heißt für mich, dass du in einer Klasse, in der das PDO Objekt steckt, die SQL Statements zusammenbauen willst. Ich hab ein paar Regeln beim Programmieren. Eine Davon ist "Trennung der Anliegen".

    Ich habe selbst eine PDO Klasse, die ein paar mehr Funktionen bereit hält als das php übliche PDO selbst. So z.B. query( $query, $data ). Das ist die Schnittstelle an der das prepare und execute passiert + Fehlerbehandlung.

    Der $query kann entweder ein String sein oder es wird ein Objekt "Query_Builder" übergeben. Dieser hilft mir beim bilden eines Querys. Da kann man z.b. einen Select hinzufügen select( $select, $alias) oder man fügt ein where() hinzu. Diese Dinge Select, Where oder was auch immer können wiederum Objekte sein. Am Ende bekomme ich vom Query_Builder ein fertiges Query getQuery().

    Was ich dir damit zeigen möchte ist, dass ich ein großes Thema in viele kleine Bausteine zerlegt habe. Die sind leichter zu verstehen und zu testen. Vor allem kann man diese sehr leicht in UnitTests packen.

    Datenbankfelder kannst du dann über "echte" Models definieren ... aber da wurde dir ja schon geholfen.

    Deswegen, hab keine Angst neue und vor allem kleine Klassen an zu legen. Teile und Herrsche !

    Gruß
    T-eilender allein Herrscher Rex

    1. Hallo T-Rex,

      ich lese da ein "self::prepare".

      Das heißt für mich, dass du in einer Klasse, in der das PDO Objekt steckt, die SQL Statements zusammenbauen willst. Ich hab ein paar Regeln beim Programmieren. Eine Davon ist "Trennung der Anliegen".

      Separation of Concerns. Ja, das kenne ich. Ich habe in meiner App-Klasse im Constructor folgende statische Instanziierung (sagt man das so?) der Database-Klasse: static::$db = new Database( $config -> db ?? [] ); In derselben App-Klasse habe ich dann noch eine statische Methode, mit der andere Klassen die Database-Klasse leicht instanziieren können:

      public static function db(): Database
      
      { return static::$db; }
      

      In der Database-Klasse steckt dann sowohl die Konfiguration für die Datenbank-Verbindung als auch die prepare-Methode für SQL-Anfragen (müsste wohl eher getPreparedStmt heißen):

      public function prepare( $sql ): PDOStatement
      
      { return $this -> pdo -> prepare( $sql ); }
      

      Diese Lösung habe ich mir nicht selbst ausgedacht, sondern sie von einem anderen MVC-Framework abgeschaut. Ich fand sie elegant und zielführend. Mit dem PDO-Modul bin ich leider kaum vertraut. Kenne nur ein paar grundlegende Befehle.

      Ich habe selbst eine PDO Klasse, die ein paar mehr Funktionen bereit hält als das php übliche PDO selbst. So z.B. query( $query, $data ). Das ist die Schnittstelle an der das prepare und execute passiert + Fehlerbehandlung.

      Der $query kann entweder ein String sein oder es wird ein Objekt "Query_Builder" übergeben. Dieser hilft mir beim bilden eines Querys. Da kann man z.b. einen Select hinzufügen select( $select, $alias) oder man fügt ein where() hinzu. Diese Dinge Select, Where oder was auch immer können wiederum Objekte sein. Am Ende bekomme ich vom Query_Builder ein fertiges Query getQuery().

      Ja, das ist durchaus sinnvoll. Wenn ich bei GitHub schaue, dann fällt mir natürlich auch auf, dass jede Methode in der Regel möglichst nur eine Sache macht. Das Ergebnis sind dann manchmal Klassen mit zig Methoden. Wahrscheinlich liegt das an meinem Skill-Level, aber ich persönlich finde tatsächlich zu kleingliedrigen „Methoden-Bau“ nicht wirklich übersichtlich. Ist wohl auch immer an das gebunden, was man letztlich mit dem Framework bzw. der Library machen möchte und für wen sie geschrieben wird. In der Regel sollen sie ja General-Purpose-Lösungen sein. Das Framework, an dem ich arbeite, ist jedoch nur an die Anliegen meiner Website angepasst.

      Was ich dir damit zeigen möchte ist, dass ich ein großes Thema in viele kleine Bausteine zerlegt habe. Die sind leichter zu verstehen und zu testen. Vor allem kann man diese sehr leicht in UnitTests packen.

      Das Thema UnitTesting habe ich mir noch gar nicht angeschaut. Das steht momentan ziemlich weit hinten auf der Agenda.

      Datenbankfelder kannst du dann über "echte" Models definieren ... aber da wurde dir ja schon geholfen.

      Was meinst du mit „echten“ Models? Was wären denn „unechte“?

      Deswegen, hab keine Angst neue und vor allem kleine Klassen an zu legen. Teile und Herrsche !

      Danke für die Tipps!

      Grüße
      Boris

      1. Hi,

        Was meinst du mit „echten“ Models? Was wären denn „unechte“?

        Kleidi Hum, Caomi Nampbell, Schlaudia Ciffer, … 😉

        cu,
        Andreas a/k/a MudGuard

      2. public function prepare( $sql ): PDOStatement

        { return $this -> pdo -> prepare( $sql ); }

        Das ist meiner Meinung nach sinnfrei. Wieso nicht $object->getPDO()->prepare( $sql ) aufrufen? Sofern die Methode nichts weiter macht sparst du dir nur das getPDO (sofern es das gibt).

        Was meinst du mit „echten“ Models? Was wären denn „unechte“?

        Arrays oder Configdateien fallen mir da ein. Gerade zu Zeiten wo XML noch der letzte Scheiß war, wurde gerne die Datenbank via XML File definiert. Für ein reines Datenbankupdate ist das natürlich okay, aber wenn sobald du auch Daten aus der Datenbank ausliest braucht man die Struktur im Code. Und schwups hat man Redundanzen. Aber ist ja nicht schlimm ... sin ja nur zwei stellen. Bis dann der Code wächst.

        Das mit dem Wachsen solltest du auch nicht unterschätzen! Schnell ist eine Methode quick-and-dirty programmiert - ist ja nicht so wichtig. Später verwendest du das ganze öfters und ärgerst dich, wieso du es nicht gleich richtig gemacht hast. Ist aber auch eine Sache von Erfahrung.

        Ich liebe Unittests. Da du deinen Code sowieso testest, wieso nicht gleich mit Unittests? Klar, so ein echo oder var_dump ist schnell geschrieben. Aber genau so schnell auch wieder gelöscht. Ein geschriebener Unittests bleibt den rest des Projektes dein Freund. Hat mir schon viele böse Fehler erspart.

        Gruß
        Getester-Rex

        1. public function prepare( $sql ): PDOStatement

          { return $this -> pdo -> prepare( $sql ); }

          Das ist meiner Meinung nach sinnfrei. Wieso nicht $object->getPDO()->prepare( $sql ) aufrufen? Sofern die Methode nichts weiter macht sparst du dir nur das getPDO (sofern es das gibt).

          Ja, stimmt, kann man sich überlegen, ob man darauf verzichten soll. Danke für den Hinweis!

          Was meinst du mit „echten“ Models? Was wären denn „unechte“?

          Arrays oder Configdateien fallen mir da ein. Gerade zu Zeiten wo XML noch der letzte Scheiß war, wurde gerne die Datenbank via XML File definiert. Für ein reines Datenbankupdate ist das natürlich okay, aber wenn sobald du auch Daten aus der Datenbank ausliest braucht man die Struktur im Code. Und schwups hat man Redundanzen. Aber ist ja nicht schlimm ... sin ja nur zwei stellen. Bis dann der Code wächst.

          Mit XML hatte ich noch nie etwas zu tun.

          Das mit dem Wachsen solltest du auch nicht unterschätzen! Schnell ist eine Methode quick-and-dirty programmiert - ist ja nicht so wichtig. Später verwendest du das ganze öfters und ärgerst dich, wieso du es nicht gleich richtig gemacht hast. Ist aber auch eine Sache von Erfahrung.

          Ja, das ist mein Anliegen bzw. wäre es schön, wenn mir das einigermaßen gelingen würde. Ich versuche immer noch alles zu optimieren. „Quick and dirty“ soll es nicht sein. Mir fehlt da einfach noch einiges an Wissen. Vor allem über Datenbank-Pflege weiß ich nichts. Ich werde aber weiter recherchieren und nachfragen.

          Ich liebe Unittests. Da du deinen Code sowieso testest, wieso nicht gleich mit Unittests? Klar, so ein echo oder var_dump ist schnell geschrieben. Aber genau so schnell auch wieder gelöscht. Ein geschriebener Unittests bleibt den rest des Projektes dein Freund. Hat mir schon viele böse Fehler erspart.

          Muss ich mal schauen, aber aktuell mache ich erst mal andere Dinge. Ein Schritt nach dem anderen, sonst werde ich verrückt. 😵

          1. Ja entweder löscht du es oder du erweiterst es und machst es sinnvoll:

             public function query( $strQuery, $arValues = array() ) {
                 $this->objPDOStatement = $this->getPDO()->prepare( $strQuery );
                 try {
                     $this->objPDOStatement->execute( $arValues );
                 } catch (Exception $e) {
                     //--- Error handling
                 }
                 return $this->objPDOStatement->fetchAll(\PDO::FETCH_ASSOC);
             }
            
             public function getLastPDOStatement() {
                 return $this->objPDOStatement;
             }
            
             public function get() {
                 if( !$this->objPDO ){
                    $this->init();
                 }
                 return $this->objPDO;
             }
            
            
            

            Das ist ein Auszug eines kleinen PDO Handlers, welchen ich erst die letzten Tage für ein aktuell sehr kleines Projekt gebastelt habe.

            Gruß
            T-queREXy