momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Aplicaţie PHP pentru operaţii CRUD pe o bază de date

CRUD | PHP | virtual host
2010 jan

Ne propunem o aplicaţie Web care să permită operaţii de listare, updatare, inserare, ştergere (pe scurt: CRUD) pentru oricare tabel existent într-o bază de date precizată.

Există numeroase framework-uri care previzionează şi modelează până la un anumit nivel, operaţiile principale într-un anumit context - inclusiv operaţii CRUD, în contextul bazelor de date. Vrem însă o aplicaţie cât se poate de simplă, care să nu se ocupe decât de CRUD - similar cu ceea ce oferă phpScaffold. Însă phpScaffold suferă de hard-coding: numele tabelului şi denumirile câmpurilor sunt înscrise direct în formulările SQL, de exemplu:

 $sql = "INSERT INTO users (nume, pren) VALUES( '{$_POST['nume']}', '{$_POST['pren']}' )";

urmarea fiind că fişierele PHP furnizate de phpScaffold sunt destinate operaţiilor CRUD pe un anumit tabel, depinzând direct de numele tabelului şi de denumirile câmpurilor acestuia.

Condiţiile asumate

Presupunem o bază de date MySQL existentă; aplicaţia nu-şi propune să creeze vreo bază de date, sau vreun tabel. Presupunem că fiecare tabel are o "cheie primară", denumită id şi declarată identic în toate tabelele:

     `id` integer unsigned not null auto_increment primary key

Fiind astfel declarate, valorile id vor fi gestionate automat de către MySQL; ca urmare, vom exclude câmpul id de pe formularele de editare şi de inserare.

Tabelele pot să aibă anumite relaţii între ele, dar - pentru simplitate - aplicaţia nu-şi propune să folosească "join"-uri între tabele, ci angajează în fiecare moment câte un singur tabel (nu şi pe cele relaţionate cu acesta).

Ar trebui să fie de la sine înţeles, că baza de date asupra căreia aplicaţia permite operaţii CRUD nu este stocată pe "Desktop" şi nu se poate deschide de pe calculatorul propriu pentru a executa direct diverse comenzi SQL. Baza de date este stocată undeva pe un anumit calculator-server, pe care este instalat un anumit software de gestiune a bazelor de date, iar accesul la bazele de date existente pe server se face în baza unor anumite drepturi prestabilite; de regulă, baza de date nu poate fi accesată direct, ci prin intermediul unui anumit software instalat pe server (de obicei PHP sau Perl).

Aplicaţia noastră va fi instalată pe server (alături de alte aplicaţii Web existente), constând în anumite fişiere PHP care să realizeze conexiunea cu MySQL şi să-i transmită comenzile SQL de executat pe calculatorul-gazdă; ea va putea fi accesată şi utilizată de pe oricare calculator, prin intermediul unui browser instalat pe acel calculator.

Intenţia aplicaţiei

Înregistrările existente sunt redate printr-un element <table>, ca în imaginea următoare:

Link-urile Edit şi Delete, adăugate pe fiecare linie - direcţionează la /edit.php?id= şi respectiv /delete.php?id=, transmiţând valoarea id a acelei linii; edit.php furnizează un formular de editare a înregistrării respective, precum şi un link de revenire la lista înregistrărilor existente.

Link-ul New Row lansează /new.php, care furnizează un formular "gol" în vederea înscrierii unei noi înregistrări în tabelul curent; formularul conţine denumirile câmpurilor (ca etichete) şi <input>-urile necesare pentru valorile de înscris. Pentru simplitate, nu ne ocupăm şi de validarea datelor introduse.

Link-ul Another Table lansează /worktable.php care produce lista tabelelor existente în baza de date precizată în /config.php, permiţând alegerea unui alt tabel pe care să se facă operaţiile CRUD.

Crearea prealabilă a unui VirtualHost

În Aplicaţii Web cu Ymacs am descris modalitatea obişnuită pentru a crea un virtual host pe un sistem Windows pe care este instalat XAMPP. Aici descriem cum procedăm pentru aceasta, în cazul unui sistem Linux. Denumim phpCRUD, atât VirtualHost-ul cât şi folderul aplicaţiei.

În fişierul /etc/hosts adăugăm linia 127.0.0.1 phpCRUD. Deschidem (cu drepturile userului root) un fişier de configurare a virtual-hostului dorit, în /etc/apache2/sites-available/:

     vb@localhost:~$ sudo emacs /etc/apache2/sites-available/phpCRUD

şi înscriem în acest fişier aceste declaraţii (minimale):

<VirtualHost *:80>
    ServerName phpCRUD
    DocumentRoot /home/vb/phpCRUD
    AddHandler application/x-httpd-php .php .html
    DirectoryIndex list.php
</VirtualHost>

În baza directivei DirectoryIndex, cererea http://phpCRUD/ va fi deservită de list.php.

Pentru a încheia procedura de creare a virtual-hostului, trebuie să mai creem un symlink în /etc/apache2/sites-enabled pentru fişierul de configurare de mai sus; putem folosi pentru aceasta programul a2ensite:

     vb@localhost:~$ sudo a2ensite phpCRUD

După restartarea serverului - prin vb@localhost:~$ sudo /etc/init.d/apache2 reload - vom putea lansa aplicaţia dintr-un browser, prin http://phpCRUD.

Fişierele constituente ale aplicaţiei

Aplicaţia noastră este constituită din câteva fişiere PHP, relativ scurte; utilizatorul va putea adăuga dacă doreşte, un fişier CSS pentru prezentarea ("stilarea") tabelelor, formularelor, link-urilor, etc.

Pentru a crea un tabel (sau un formular) nu vom folosi direct HTML, ci vom proceda după următoarea schemă (întâlnită mai ales în programe javaScript): se pleacă de la un tablou PHP $table = array(); în acest tablou se înscriu pe rând (ca şiruri) definiţiile HTML necesare, pe măsură ce sunt generate (printr-un "for") datele de înscris: întâi $table[] = "<table border='1'><tr>";, apoi $table[] = "<th>" . $nume_câmp_1 . "</th>"; ş.a.m.d. - încheind cu $table[] = "</table>";. Scrierea tabloului PHP final $table în document (în fereastra browserului) se va face folosind echo implode('', $table);.

config.php

Aici trebuie înscris numele bazei de date pe ale cărei tabele se intenţionează operaţii CRUD. Prin intermediul acestui fişier, PHP va realiza conexiunea cu MySQL şi cu baza de date indicată.

<?php
$my_database = "testwork";

$dbi = new mysqli('phpCRUD', 'vb', '123456', $my_database);
if($dbi->connect_errno) die("Connect failed: " . $dbi->connect_error); 

// echo $dbi->character_set_name();
$dbi->set_charset("utf8");
?> 

Dar atenţie: conexiunea nu este persistentă - HTTP tratează fiecare cerere independent de cererile precedente. Ca urmare, fişierul config.php va trebui să fie inclus în fiecare dintre celelalte fişiere, pentru reconectare; acesta este şi motivul pentru care s-a constituit un fişier separat, pentru conectarea cu baza de date.

worktable.php

Pentru a păstra numele tabelului de lucru curent, în toate invocările de fişiere PHP destinate diverselor operaţii cu acel tabel - folosim sesiuni de comunicare (sessions). Funcţia PHP session_start() trebuie să figureze neapărat la începutul programului (imediat după tag-ul <?php) şi ea asigură posibilitatea de a înscrie sau de a folosi valorile existente în variabila $_SESSION (tablou asociativ).

Este suficient să se elimine din sesiunea curentă numele tabloului (folosind unset()) şi să se redirecteze la /list.php — acesta - negăsind valoarea tocmai ştearsă, $_SESSION['mytable'] - va afişa lista tabelelor din baza de date, permiţând o nouă alegere pentru tabelul de lucru.

<?php session_start();
unset($_SESSION["mytable"]);

echo '<script type="text/javascript">window.location="list.php";</script>';
?>

Pentru redirectarea dorită, am inclus un <script> pentru a seta window.location (= noua adresă).

list.php

Se instituie variabila $mytable (numele tabelului de lucru), după caz: dacă există $_SESSION['mytable'] atunci aceasta devine valoarea variabilei $mytable şi se trece la SELECT-area înregistrărilor acelui tabel şi redarea lor printr-un <table>; altfel, utilizatorul va trebui să aleagă unul dintre tabelele din "meniul" afişat şi la click pe butonul "Select Table" variabila $mytable va primi ca valoare $_POST['mytable'], adică numele ales din meniu (şi apoi, se trece la SELECT-area şi redarea înregistrărilor din tabelul ales).

Lista tabelelor existente se obţine invocând SHOW TABLES; numele acestora sunt înscrise în elemente <option>, conţinute într-un <select name='mytable'>. Când utilizatorul va folosi "submit"-ul furnizat (adică, va face click pe butonul "Select Table"), se va transmite serverului opţiunea selectată, iar PHP va înscrie valoarea respectivă în tabloul $_POST; programul nostru va prelua această valoare în variabila proprie $mytable.

Pe tabelul indicat de $mytable se fac două interogări: DESCRIBE permite obţinerea numelor câmpurilor (valorile coloanei Field) din tabelul respectiv, în scopul constituirii "antetului" tabelului în care se vor prezenta înregistrările existente; apoi, SELECT * FROM va furniza tabloul întegistrărilor.

<?php session_start();
require('config.php');

if(isset($_SESSION["mytable"])) $mytable = $_SESSION["mytable"];
else {
    if(isset($_POST["submitmytable"])) {
        $mytable = $_POST["mytable"];
        $_SESSION["mytable"] = $mytable;
    }
    else {
        $res = $dbi->query("SHOW TABLES");
        echo "Select a work-table from the database <b>" . $my_database . "</b>: ";
        echo "<form action='' method='post'><select name='mytable'>";
        while($row = $res->fetch_assoc()) {
            foreach($row as $k => $v) {
                echo "<option value='" . $v . "'>" . $v . "</option>";
            }
        }
        echo "</select> <input type='submit' value='Select Table' />";
        echo "<input type='hidden' value='1' name='submitmytable' /></form>";
    }
}

if(isset($mytable)) {
    $thead = array("<table border='1'><tr>");
    $field_names = $dbi->query("DESCRIBE " . $mytable);
    while($row = $field_names->fetch_assoc()) { 
        $thead[] = "<th>" . $row['Field'] . "</th>";
    }
    $thead[] = "</tr>";
    $tbody = array();
    $result = $dbi->query("SELECT * FROM " . $mytable);
    if($result->num_rows > 0) {
        while($row = $result->fetch_assoc()) {
            $tbody[] = "<tr>"; 
            foreach($row as $key => $val) {
                $tbody[] = "<td>" . stripslashes($val) . "</td>";
            }
            $tbody[] = "<td><a href=edit.php?id={$row['id']}>Edit</a>" . 
                       " | <a href=delete.php?id={$row['id']}>Delete</a></td></tr>";
        }
    } 
    $tbody[] = "</table>";
    echo implode('', $thead) . implode('', $tbody);
    echo "<a href='new.php'><b>New Row</b></a>" .
         " | <a href='worktable.php'><b>AnotherTable</b></a>"; 
}
?>

De regulă, valorile primite de la utilizator sunt trecute printr-un proces de escapare, înainte de a fi în final înscrise în baza de date; aceasta înseamnă în esenţă mascarea caracterelor "periculoase", de exemplu prin prefixarea acestora cu caracterul backslash (evitând astfel înscrierea de elemente <script>, de exemplu - care va fi înscris eventual ca secvenţă "\<script\>", pierzându-şi astfel calitatea de "program executabil"). Aceasta înseamnă că (invers) atunci când se extrag date (ca în list.php), acestea trebuie să fie "curăţăte" de posibilele slash-uri de escapare - folosind stripslashes() ca mai sus.

Tabelul care prezintă înregistrările a fost completat cu linkuri Edit şi Delete (pentru fiecare înregistrare), precum si cu linkuri pentru inserarea unei noi înregistrări şi pentru revenirea la secvenţa de alegere a unui alt tabel (New Row şi respectiv Another Table, adăugate dedesubtul tabelului).

new.php

vizează inserarea unei noi înregistrări. Dar nu se oferă şi câmpul id, fiindcă acesta reprezintă primary key şi fiind declarat auto_increment, va fi gestionat automat de către MySQL.

<?php session_start(); 
$mytable = $_SESSION["mytable"]; 
require('config.php'); 

if (isset($_POST['submitted'])) { 
    $field_list = array();
    $values_list = array();
    foreach($_POST as $key => $value) {
        if($key != "submitted") {
            $field_list[] = $key;
            $values_list[] = "'" . $dbi->real_escape_string($value) . "'";
        }
    }
    $sql = "INSERT INTO " . $mytable . " (" . implode(',', $field_list) . 
                               ") VALUES(" . implode(',', $values_list) . ")"; 
    $dbi->query($sql); 
    echo "<a href='list.php'><b>To Listing</b></a>"; 
} 

$result = $dbi->query("DESCRIBE " . $mytable);
$form = array("<form action='' method='POST'>");
while($row = $result->fetch_assoc()) { 
    $field = $row['Field'];
    if($field != 'id') {
        $form[] = "<p>" . $field . ": <input type='text' name='".$field."' /></p>";
    }
}
$form[] = "<p><input type='submit' value='Add Row' />" .
          "<input type='hidden' value='1' name='submitted' /></p>";
$form[] = "</form>";
echo implode('', $form);
?>

Formularul de inserare conţine desigur, numele câmpurilor - obţinute prin preluarea valorilor din coloana Field a tabelului furnizat de DESCRIBE - şi elemente <input> pentru introducerea valorilor. La trimiterea acestui formular, PHP va pune în tabloul $_POST valorile înscrise în <input>-uri (sub forma nume => valoare) şi programul nostru le va escapa folosind real_escape_string() şi va trimite spre execuţie comanda INSERT INTO.

edit.php

permite editarea câmpurilor înregistrării al cărei id îi este trimis (prin GET, de la link-ul "Edit" corespunzător înregistrării în tabelul prezentat de list.php); desigur, din lista câmpurilor se elimină id (fiindcă acesta reprezintă cheia primară a tabelului şi nu-i treaba utilizatorului să-l editeze).

<?php session_start();
require('config.php');

$mytable = $_SESSION["mytable"]; 

if (isset($_GET['id'])) { 
    $id = (int) $_GET['id'];
    if (isset($_POST['submitted'])) { 
        $key_val = array();
        foreach($_POST as $key => $val) {
            if($key != "submitted") {
                $key_val[] = $key . "='" . $dbi->real_escape_string($val) . "'";
            }
        }
        $sql = "UPDATE " . $mytable . "  SET " . implode(',', $key_val) .
                         "  WHERE id='".$id."'";
        $dbi->query($sql); 
        echo "<a href='list.php'><b>To Listing</b></a>"; 
    } 

    $row = $dbi->query("SELECT * FROM ".$mytable." WHERE id='".$id."'")->fetch_assoc();
    $form = array("<form action='' method='POST'>");
    foreach( $row as $key => $val) {
        if($key != 'id') {
            $form[] = "<p>" . $key . ": <input type='text' name='" . $key . "' value='" . 
                      stripslashes($val) . "' /></p>";
        }
    }
    $form[] = "<p><input type='submit' value='Edit Row' />" .
                  "<input type='hidden' value='1' name='submitted' /></p></form>";
    echo implode('', $form);
}
?>

Pentru înscrierea noilor valori în câmpurile înregistrării respective am folosit UPDATE...WHERE id=.

delete.php

şterge înregistrarea cu id-ul primit (prin GET, de la link-ul "Delete" din tabelul prezentat de /list.php) şi redirecţionează la /list.php (implicând obiectul javaScript window.location).

<?php session_start();
include('config.php'); 
$mytable = $_SESSION["mytable"]; 

$id = (int) $_GET['id']; 
$dbi->query("DELETE FROM " . $mytable . " WHERE id = '$id'") ; 

echo '<script type="text/javascript">window.location="list.php";</script>';
?> 

SYNOPSIS

— constituie un VirtualHost pentru aplicaţie

— copiază fişierele de mai sus (sau descarcă phpCRUD.zip) în directorul de DocumentRoot

— deschide config.php şi modifică numele pentru baza de date, host, user şi parolă

— probează aplicaţia, lansând dintr-un browser

vezi Cărţile mele (de programare)

docerpro | Prev | Next