Michael Dittrich: PHP PDO Problem

Hallo,

ich hoffe es gibt jemanden der mir helfen kann. Ich versuche mich gerade in PDO und möchte meine Webseiten umrüsten.

Dabei bin ich auf folgenden Problem gestoßen:

Ich habe bisher folgenden Code:

Edit Rolf B: Ich habe den Init von s_fahrzeugnummer und das SELECT-Statement auf mehrere Zeilen verteilt. Beim SELECT habe ich Heredoc draus gemacht; im Original war's ein double-quote String der 10 Meter lang ist. Ab PHP 7.3 kann END und ); auf eine Zeile.

  $fahrzeugnummer = "101 019, 103 245, 120 001, 481 100";

  $s_fahrzeugnummer = (strstr($fahrzeugnummer,","))
     ? str_replace(",","%' OR `fahrzeugnummer` LIKE '%",$fahrzeugnummer)
     : $fahrzeugnummer;

  $zusatz = $db_rev->prepare(<<<END
SELECT `datensatz_fahrzeug`, `fahrzeugnummer`, `belegung`,
       `betreiberbezeichnung`, `bahnverwaltung`, `fahrzeugnutzer`,
       `letztes_update`, `tabellennummer`
FROM `tab_fahrzeuge`
WHERE `fahrzeugnummer` LIKE :s_fahrzeugnummer
UNION
SELECT `datensatz_fahrzeug`, `ex_nummer`, `belegung`,
       `betreiberbezeichnung`, `bahnverwaltung`, `fahrzeugnutzer`,
       `letztes_update`, `tabellennummer`
FROM `tab_exnummern`
WHERE `ex_nummer` LIKE :s_fahrzeugnummer 
UNION 
SELECT `datensatz_fahrzeug`, `fahrzeugnummer`, `belegung`,
       `betreiberbezeichnung`, `bahnverwaltung`, `fahrzeugnutzer`,
       `letztes_update`, `tabellennummer` 
FROM `tab_vermietung` 
WHERE `fahrzeugnummer` LIKE :s_fahrzeugnummer 
ORDER BY `tabellennummer`, `fahrzeugnummer`
END
);

  $zusatz->execute(array('s_fahrzeugnummer' => "%$s_fahrzeugnummer%"));

  $max_fahrzeuge = $zusatz->rowCount();

  echo $max_fahrzeuge;

Damit kann ich im Eingabefeld eingeben: 101 019, 103 245, 120 001, 481 100 Er sucht dann die vier Fahrzeuge (um die geht es auf meiner Webseite) raus.

In PDO kommt 0 Datensätze raus!

Ich verstehe das nicht. Leider habe ich auch keine Möglichkeit gefunden, anzeigen zulassen was bei execute aus den Platzhaltern wird.

Wenn ich hier falsch bin, bitte ich um Nachricht und Löschung meines Beitrages.

Gruß, Michael

akzeptierte Antworten

  1. Hallo Michael,

    ist schon okay, dass Du hier fragst. Man sagt uns nach, gelegentlich schon 2-3 Worte PHP gesprochen zu haben.

    Meine Hinweise:

    Der monströse SELECT. Ich habe das editiert und HEREDOC Syntax draus gemacht. Deutlich lesbarer, gelle?

    Wenn Code nicht tut, was man erwartet, muss man debuggen. Ich habe das Problem jetzt durch Anstarren gefunden (auch nicht im ersten Anlauf), aber wenn ich PHP schreibe, habe ich immer eine Log-Funktion, die Protokolleinträge in einem Array sammelt und am Seitenende raushaut. Wenn der Code produktiv läuft, mache ich aus der Log-Funktion einen Dummy, der nichts tut. Alternativ kannst Du Log-Infos auch mit error_log ins PHP Log schreiben, meine Erfahrung ist nur, dass das relativ langsam ist.

    Damit kannst Du dann Variablenwerte protokollieren und schauen, ob die erwarteten Werte herauskommen. Wenn nicht, kannst Du die Problemstelle eingrenzen.

    Was habe ich gefunden:

    Du versuchst, eine kommaseparierte Liste zu erkennen und daraus mehrere LIKEs zu machen. Kann man machen, ist aber mühsam. Und vor allem geht das nicht, wenn Du SQL Parameter verwendest. Innerhalb eines Parameterwertes darf keine SQL Syntax vorkommen - na gut, darf schon, wird aber als zu suchender Wert und nicht als SQL interpretiert. Und weil die Fahrzeugnummern in deiner DB nicht das Wort LIKE enthalten, findest Du nichts.

    Hinzu kommt die exnummern-tabelle, da heißt die Spalte nicht fahrzeugnummer sondern exnummer, deine LIKE-Verkettung sucht aber fahrzeugnummer. Das gibt einen SQL Fehler, er wird die Ausführung gar nicht erst versuchen.

    Drittens hast Du hinter den Kommata in der Nummernliste Leerstellen. Die schmeißt Du beim Erzeugen der Like-Liste nicht weg, d.h. du würdest explizit nach diesem Space suchen. Was mutmaßlich schiefgeht. Aber wie gesagt: so weit kommst Du gar nicht.

    Wenn die Fahrzeugnummernfragmente, die Du suchst, ausschließlich aus Ziffern und Leerstellen bestehen, kannst Du eventuell mit REGEXP arbeiten. Das hängt von deiner Datenbank ab. Welche hast Du? Und zwar ganz genau?

    Andernfalls musst Du den FROM-Teil des SQL von Hand zusammenklöppeln. Geht, ist aber etwas umständlich. Darüber reden wir dann, wenn's nötig ist. Und wenn's nicht nötig ist, reden wir über REGEXP.

    Das wichtigste ist: Das Ergebnis von $statement->execute() muss überprüft werden. IMMER IMMER IMMER. Ist es FALSE, ging in der Query etwas schief und du hast kein valides Ergebnis. In $statement->errorInfo() findest Du den Grund, warum es schiefging (das ist ein Array mit mehreren Einträgen, entweder haust Du die mit var_dump() zum Browser raus oder loggst sie sorgfältig. Ignorieren geht hingegen gar nicht).

    Rolf

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

      ist schon okay, dass Du hier fragst. Man sagt uns nach, gelegentlich schon 2-3 Worte PHP gesprochen zu haben.

      Vielen Dank, dass ich hier aufgenommen wurde.

      Meine Hinweise:

      Der monströse SELECT. Ich habe das editiert und HEREDOC Syntax draus gemacht. Deutlich lesbarer, gelle?

      Ich habe darüber nie nachgedacht. Alles was ich über SQL und PHP weiß, habe ich mir selbst beigebracht. Aber ja, dass ist wirklich besser.

      Wenn Code nicht tut, was man erwartet, muss man debuggen. Ich habe das Problem jetzt durch Anstarren gefunden (auch nicht im ersten Anlauf), aber wenn ich PHP schreibe, habe ich immer eine Log-Funktion, die Protokolleinträge in einem Array sammelt und am Seitenende raushaut. Wenn der Code produktiv läuft, mache ich aus der Log-Funktion einen Dummy, der nichts tut. Alternativ kannst Du Log-Infos auch mit error_log ins PHP Log schreiben, meine Erfahrung ist nur, dass das relativ langsam ist.

      Ich habe eine Log-Datei, aber da erscheint keine Fehlermeldung!

      Damit kannst Du dann Variablenwerte protokollieren und schauen, ob die erwarteten Werte herauskommen. Wenn nicht, kannst Du die Problemstelle eingrenzen.

      Was habe ich gefunden:

      Du versuchst, eine kommaseparierte Liste zu erkennen und daraus mehrere LIKEs zu machen. Kann man machen, ist aber mühsam. Und vor allem geht das nicht, wenn Du SQL Parameter verwendest. Innerhalb eines Parameterwertes darf keine SQL Syntax vorkommen - na gut, darf schon, wird aber als zu suchender Wert und nicht als SQL interpretiert. Und weil die Fahrzeugnummern in deiner DB nicht das Wort LIKE enthalten, findest Du nichts. Hinzu kommt die exnummern-tabelle, da heißt die Spalte nicht fahrzeugnummer sondern exnummer, deine LIKE-Verkettung sucht aber fahrzeugnummer. Das gibt einen SQL Fehler, er wird die Ausführung gar nicht erst versuchen.

      Drittens hast Du hinter den Kommata in der Nummernliste Leerstellen. Die schmeißt Du beim Erzeugen der Like-Liste nicht weg, d.h. du würdest explizit nach diesem Space suchen. Was mutmaßlich schiefgeht. Aber wie gesagt: so weit kommst Du gar nicht.

      Ich habe mal ein Bild gemacht. Er zeigt als Fehlercode: 0000 an! Die Leerzeichen habe ich korrigiert.

      Wenn die Fahrzeugnummernfragmente, die Du suchst, ausschließlich aus Ziffern und Leerstellen bestehen, kannst Du eventuell mit REGEXP arbeiten. Das hängt von deiner Datenbank ab. Welche hast Du? Und zwar ganz genau?

      Ich habe MariaDB 10.11.13

      Die Fahrzeugnummern haben sich, geschichtlich bedingt, mehrmals geändert. Ich betreibe mit weiteren Leuten eine Datenbank über Revisionsdaten der Eisenbahn.

      Das mit der Ex-Nummer ist natürlich richtig, das müsste ich dann in der Datenbank ändern. Wäre aber kein Problem, aus ex-nummer eine fahrzeugnummer zu machen.

      Viele Grüße Michael

      Ausgabe auf einer Testseite

      1. Nachtrag:

        Ich habe das mal bei PHPmyAdmin eingegeben

        Dabei habe ich die ex-nummer in fahrzeugnummer umbenannt.

        Das Ergebnis hänge ich mal als Bild an.

        Gruß, MichaelPHPmyAdmin

      2. Hallo Michael,

        das Grundproblem besteht weiter: in s_fahrzeugnummer befindet sich SQL Syntax, von der Du erwartest, dass sie als SQL ausgeführt wird.

        Das von Dir gezeigte phpmyAdmin-SQL ist nicht äquivalent zu dem von Deinem PHP abgeschickten SQL. Was Du da versuchst, ist SQL Injektion, und genau die wird von Query-Parametern verhindert.

        Wenn Du SQL Injektion haben willst, musst Du das im SQL Statement tun, nicht in den Query-Parametern.

        Funktionierende SQL Injektion:

        $x = "123%' OR dings LIKE '%456";
        $sql = "SELECT a,b,c FROM tab WHERE dings LIKE '%$x%'";
        $statement = $db->query($sql);
        

        Ausgeführt wird

        SELECT a,b,c
        FROM   tab
        WHERE  dings LIKE '%123%' OR dings LIKE '%456%'
        

        Dein Versuch, mit SQL Parametern:

        $x = "123' OR dings LIKE '%456";
        $sql = "SELECT a,b,c FROM tab WHERE dings LIKE :x_var";
        $statement = $db->prepare($sql);
        $statement->execute([ "x_var" => "%$x%" ]);
        

        Ausgeführt wird

        SELECT a,b,c
        FROM tab
        WHERE dings LIKE '%123%\' OR dings LIKE \'%456%'
        

        Siehst Du den Unterschied?

        (Ich speichere das erstmal um zu sehen ob das Forum das brauchbar highlighted, es geht noch weiter)

        Rolf

        --
        sumpsi - posui - obstruxi
      3. Hallo Michael,

        so, Highlighting funktioniert, so dass Du siehst, was dein Versuch mit einer Variablen anrichtet.

        Wenn Du SQL Syntaxteile dynamisch einsteuern willst - und der LIKE Operator ist SQL Syntax - dann musst Du mit SQL Injektion arbeiten. Sprich: Du musst das SQL komplett von Hand aufbauen und dabei auch auf korrekten Kontextwechsel achten. Jeder Parameterstring, den Du in SQL einbaust, muss mit $db->quote behandelt werden.

        In deinem Fall ist das aber nicht unbedingt nötig. Du hast MariaDB und das kennt außer LIKE auch die Mustersuche mit REGEXP. Wenn dein Suchstring so aussieht:

        "101 019, 103 245, 120 001, 481 100";

        dann kannst Du ihn in dies hier transformieren:

        "101 019|103 245|120 001|481 100";

        Und dann mit dem REGEXP Operator an Stelle von LIKE arbeiten:

        $sql = <<<END
           SELECT ...
           FROM ...
           WHERE fahrzeugnummer REGEXP :s_fahrzeugnummer
           UNION
           ...
           END;
        

        Damit wird es funktionieren. Der senkrechte Strich ist der "ODER"-Operator in einer Rexexp, wenn Du mehrere Suchmuster getrennt durch senkrechte Striche zusammenfügst, trifft die Regexp zu, wenn eins der Suchmuster zutrifft. MariaDB verwendet ähnlich wie PHP die PCRE-Suchmuster.

        Ein schöner Spielplatz für PCRE-RegExp ist regex101.com. Eine Einführung die Syntax von MariaDB-Regexp findest Du hier, leider nicht auf deutsch. Wichtig für REGEXP in MariaDB ist, dass der aus Perl oder JavaScript bekannte REGEXP-Delimiter wegfällt. regex101 zeigt Dir vor und hinter der Regexp ein / an, das wäre dieser Delimiter, den gibt's in MariaDB nicht.

        Die Prozentzeichen um die Nummern brauchst Du bei REGEXP nicht, der macht standardmäßig eine "ist irgendwo im String enthalten" Suche. Bei REGEXP musst Du extra angeben, falls Du möchtest, dass dein Muster am Anfang oder Ende steht. Aber das ist bei Dir ja nicht so.

        Voraussetzungen

        • Du schmeißt die Spaces hinter den Kommas weg. In deiner phpMyAdmin-Query hast Du das getan, dein gezeigter PHP Code tut es nicht.
        • Du bist sicher, dass im Suchstring keine Regex-Steuerzeichen stehen. Davon gibt es eine ganze Menge. Wenn dein Suchstring mehr enthält als Buchstaben, Ziffern und einen Bindestrich, dann behandle jede einzelne Fahrzeugnummer im PHP mit preg_quote (2. Parameter weglassen)

        Eine Funktion, die deinen Suchstring umbaut, könnte so aussehen:

        function suchstringAlsRegExp($such) {
        	$teile = explode(",", $such);
        	$teile = array_map(fn($teil) => preg_quote(trim($teil)), $teile);
        	return implode('|', $teile);
        }
        

        oder, für Fans einzeiliger Logik beliebiger Komplexität, so:

        function suchstringAlsRegExp2($such) {
        	return implode('|', array_map(fn($teil) => preg_quote(trim($teil)), explode(",", $such)));
        }
        

        array_map ist ganz praktisch, um jedes einzelne Element eines Arrays mit einer Funktion zu behandeln. Das ist hier eine Pfeilfunktion:

        fn($teil) => preg_quote(trim($teil))
        

        die dem Suchstring-Teil die Leerzeichen vorn und hinten wegtrimmt und ihn dann mit preg_quote gegen RegExp_Steuerzeichen absichert.

        Ich hoffe, ich überfordere damit deine PHP Erkenntnisfähigkeiten nicht 😉

        Rolf

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

          bis hierher erst einmal recht herzlichen Dank!

          Da werde ich mich am Wochenende in Ruhe mit befassen und ausprobieren. Ich werde mich melden, was meine Versuche ergeben haben.

          Viele Grüße und ein schönes Wochenende, Michael

          1. Hallo Rolf,

            ich möchte mich noch einmal herzlichst bedanken!

            Du hast meinen Horizont erweitert und mir etwas Neues beigebracht. Das funktioniert alles sehr gut. 😀

            Viele Grüße Michael