Ankündigung

Einklappen
Keine Ankündigung bisher.

PHP/MySQL: QCodo's Code-Generator

Einklappen

Neue Werbung 2019

Einklappen
Dieses Thema ist geschlossen.
X
X
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • PHP/MySQL: QCodo's Code-Generator

    Hallo,

    schon seit laengerem wollte ich euch mal QCodo vorstellen, das in Europa noch nicht sehr weit verbreitet zu sein scheint, hier in den USA ist es mir jedoch schon oefter untergekommen, nicht nur weil es unser Standard-Framework hier in der Agentur ist. Ist aber einen Blick wert Kann sein dass ich dazu etwas ausschweife, ich werde auch kurz meinen Programmierstil vorstellen und auch nicht zutief in die Materie eindringen, aber zumindest die erste Huerde sollte dadurch genommen werden


    QCodo/ZCodo's OR-Mapper (Object-Relational-Mapping) wird als Input mit einem MySQL-Datenbank-Schema gefuettert und parsed daraus PHP-Code, der einem volle CRUD-Funktionalitaet (Create, Read, Update, Delete) bietet. Zusaetzlich werden automatisch oder ueber Konfiguration festgelegte Beziehungen zu anderen Tabellen mit in die Klassen-Generierung einbezogen. Auch werden indizierte Spalten oder UNIQUE-Felder beruecksichtigt, in dem Zugriffsmethoden fuer die Klasse generiert werden.


    Auch haben die Entwickler etwas nachgedacht und generieren nicht nur eine Klasse, sondern gleich 2, denn schliesslich moechte man der Klasse eigene Funktionalitaeten hinzufuegen, die bei einer Neugenerierung (z.B. nach Schemaaenderung) nicht gleich wieder ueberschrieben werden sollen. Fuer eine Tabelle namens "Article" bekommen wir also die leere Klasse "Article", die wir mit unserem Code erweitern koennen, sowie die Klasse "ArticleGen", von der "Article" erbt und die QCodo bei jeder Code-Generierung ueberschreibt.


    Unsere Systemvoraussetzungen sind PHP 5 und die MySQL-Engine innoDB. Letzteres ist wichtig, da wird die Beziehung der Tabellen untereinander implizit und nicht explizit in der Konfiguration festlegen.


    Wenn ihr dem Tutorial nachprogrammieren wollt, installiert jetzt bitte QCodo:
    http://www.qcodo.com/downloads/
    und konfiguriert es, so dass ihr die Startseite ohne Fehler (magic_quotes_gpc-Meldung) erhaltet. Als Quickfix:
    Einfach in "wwwroot/includes/configuration.inc.php", der Konstanten __DOCROOT__ ein dirname(__FILE__) . "/.." zuweisen und gegebenenfalls eine ".htaccess" Datei mit "php_flag magic_quotes_gpc off" in den Ordner "wwwroot" legen, ganz nach unten scrollen und die Datenbankverbindung eingeben, fertig.


    Ich verzichte heute mal auf das Alben/CDs Beispiel und tue so, als wolle ich mit QCodo einen kleinen Shop mit Usern, Kategorien und Artikeln basteln. Verwenden wir der Einfachheit halber ein einfaches (uebrigens falsches) Konzept:

    Category, User, Cart, Article, Cart_Ref, Billing.

    Ich werde hier nur kurz auf User, Article und Category eingehen, zum Testen koennt ihr ja die restlichen Tabellen/Klassen verwenden. Es geht wirklich schnell! Auch muesst das Konzept nicht 1:1 uebernehmen, vielleicht lernt ihr mehr, wenn ihr euer Schema selbst erstellt. Achtet darauf, die Tabellen in Einzahl zu benennen und die Primaerschluessel mit "id" zu benennen. Spalten oder Tabellennamen die in MySQL mit _ versehen werden, werden von QCodo in PHP in Camel-Schreibweise umgewandelt. Aus der Tabelle "Cart_Ref" wird also die PHP-Klasse "CartRef" (und "CartRefGen"), aus "user_name" wird "UserName". Auch verwendet QCodo intern einen 3-Zeichen-Prefix fuer Variablen, "obj" also fuer Objekte, "str" fuer Strings etc. Methodennamen beginnen mit einem Grossbuchstaben. Ich kann mein Coding immer relativ schnell umschalten, auf das jeweilige Framework und ich finde es lohnt sich, ein einheitliches Coding pro Projekt zu verwenden. Aber das ist hier zweitrangig.


    Hier das Schema, nehmen wirs nicht so genau:



    Im Anhang findet ihr den von mir verwendeten SQL-Code, die Verbindungen sind ON DELETE/UPDATE CASCADE, mit Ausnahme der Article/Category Beziehung (zur Veranschaulichung), hier haben wir eine 0:1 Beziehung (NULL fuer category_id erlaubt -> ON DELETE SET NULL).


    Wenn wir nun den Codegenerator aufrufen, damit er unsere Datenbank scannt:
    /_devtools/codegen.php
    .. sollte die folgende Ausgabe erscheinen:

    Code:
    There were 6 tables available to attempt code generation:
    
    Successfully generated DB ORM Class:   Article (with 1 relationship)
    Successfully generated DB ORM Class:   Billing (with 1 relationship)
    Successfully generated DB ORM Class:   Cart (with 1 relationship)
    Successfully generated DB ORM Class:   CartRef (with 2 relationships)
    Successfully generated DB ORM Class:   Category (with no relationships)
    Successfully generated DB ORM Class:   User (with no relationships)

    Wir finden unsere Klassen in includes/data_classes bzw. includes/data_classes/generated. Unabhaengig davon wurden uebrigens auch DataGrid und Form-Klassen erstellt, mit deren Hilfe ihr eure Tabellen fuellen koennt. Dazu stellt QCodo ein eigenartiges, aber auch ziemlich geniales Formular-Management zur Verfuegung, welches aber nicht Teil dieses Tutorials ist. Schaut euch dazu aber einfach mal die "Examples" der QCodo-Website an, man braucht einige Zeit um einzusteigen, aber wenn man QCodo fuers OR-Mapping verwendet, sollte man nach einiger Zeit auch mit QForms zurechtkommen.


    Zurueck zum OR-Mapping. Schaut euch die generierten Klassen einmal an (z.B. Outline-View von Eclipse/PDT), ihr werdet sehen, dass in "UserGen" die statischen Zugriffsmethoden LoadById (Alias Load), LoadByEmail und LoadByName generiert wurden, ganz nach unserem Schema, in der diese Spalten indiziert (Primaerschluessel) bzw. als UNIQUE gesetzt wurden. Zugriff auf die Inhalte der Datenbank erhaltet ihr also so:


    PHP-Code:
    <?php
    $objUser 
    User::Load(1); // id = 1
    $objUser User::LoadByName("Fritz"); // name = Fritz
    $objUser User::LoadByEmail("fritz@gibtsgar.net"); // email = ..
    ?>

    Ihr koennt das Objekt nun nach belieben manipulieren:
    PHP-Code:
    <?php
    $objUser 
    User::Load(1);
    if (!
    $objUser instanceof User) { // im Fehlerfall ist $objUser NULL
      
    throw new Exception(..); 
    }
    $objUser->Name "Max";
    $objUser->Email "max@gibtsgar.net";
    $objUser->Save();
    ?>

    Beachtet, dass moeglicherweise beim Speichern eine Q..Exception geworfen wird, z.B. wenn ein NOT NULL Feld NULL geblieben ist oder andere Fehler aufgetreten sind. Einen User anlegen, loeschen oder kopieren geht natuerlich auch:
    PHP-Code:
    <?php
    $objUser 
    = new User();
    // oder ihr schreibt euch eine statische Methode fuer User:
    $objUser User::CreateNew("Peter""peter@gibtsgar.net");
    $objUser->Save();
    // loeschen:
    $objUser->Delete();
    ?>
    Zum Kopieren koennt ihr die Parameter der Save-Methode verwenden.


    Vergesst aber nicht, dass die *Gen-Klassen fuer Erweiterungen tabu bleiben, sonst zerstoert ihr das Konzept des OR-Mappings bei QCodo.


    Wenn wir nun unsere generierte "ArticleGen" Klasse anschauen, werden wir sehen, dass auch hier unsere Beziehung mit LoadArrayByCategoryId() umgesetzt wurde. Die Methode ist statisch und liefert uns als Ergebnis einen Array aller "Article" Objekte:

    PHP-Code:
    <?php
    foreach (Article::LoadArrayByCategoryId(5) as $objArticle) {
      
    printf("<h1>%s<h1>

    %s</p>"
    $objArticle->Name$objArticle->Description);
    }
    ?>
    Meine Vorgehensweise beim URL-gesteuerten Anzeigen von Objekten ist dann uebrigens die, dass ich jedem Objekt eine AutoloadId() bzw. Autoload() Methode spendiere, die etwa so aussieht:

    PHP-Code:
    <?php
    class Category .. {
      
    // ..
      
    public static function AutoloadId() {
        return (int)@
    $_REQUEST["category_id"];
      }
      public static function 
    Autoload() {
        return 
    self::Load(self::AutoloadId());
      }
      
    // ..
    }
    ?>

    Jede Klasse kennt also ihren Parameter, mit dem sie angesprochen wird, und so bleibt jede Information dort, wo sie hingehoert. Im Code saehe der Zugriff auf diese Artikel also bei mir etwa so aus:
    PHP-Code:
    <?php
    try {
      
    $objCategory Category::Autoload();
      if (!
    $objCategory instanceof Category) {
        throw new 
    CategoryException(CategoryException::IdNotFoundCategory::AutoloadId());
      }
      
    $objTemplate = new Template();
      
    $objTemplate->SetContext($objCategory);
      
    $objTemplate->Render();
    } catch (
    ApplicationException $objExc) {
      
    $objExc->Run();
    }
    ?>

    Wobei die Exceptions und das Templating Eigenbau sind. Mit SetContext merge ich $objCategory gewissermassen ins $this-Objekt von Template, um direkt darauf zuzugreifen. Siehe <h1> Tag, der die Name-Eigenschaft fuer das Category-Objekt abgreift. Technisch umgesetzt mit __get/__set.


    Also meine Template-Dateien sehen dann etwa so aus:

    article_list.tpl.php
    PHP-Code:
    <? $this->Header->Render() ?>
    <h1><?= $this->Name ?></h1>
    <? foreach ($this->GetArticles() as $objArticle): ?>

    <? endforeach ?>
    <? $this->Footer->Render() ?>
    (ich benutze fuer Templates keine ; und nur die : end* Syntax, wenn man das Konzept durchzieht kommt eine ziemlich uebersichtliche Sache bei raus)


    GetArticles() ist wiederum eine selbstgeschriebene Methode, die eigentlich nur folgendes macht:
    Category.class.php
    PHP-Code:
    <?php
    public function GetArticles() {
      return 
    Article::LoadArrayByCategoryId($this->Id);
    }
    ?>

    Sieht aber schoener aus (auch wenn ich dadurch eine neue Abhaengigkeit schaffe) und ausserdem kann ich dann zusaetzliche Filter einbauen, die dann nicht im Template stehen muessen. Jeder LoadArrayBy*() Funktion koennen uebrigens Clauses uebergeben werden, also Sortierungen oder neue Bedingungen. Stellt euch vor ihr habt noch einen "Visible" Flag oder aehnliches. Wenn ihr darauf einen Index setzt, koennt ihr aber uebrigens auch dafuer die Methode generieren lassen, sie hiesse dann
    LoadArrayByCategoryIdVisible().


    Manchmal spendiere ich auch eine GetSiblings() Methode, die sich eventuell auch fuer "Article" anbieten wuerde:
    PHP-Code:
    <?php
    class Article .. {
      
    // ..
      
    public function GetSiblings() {
        return 
    self::LoadArrayByCategoryId($this->CategoryId);
      }
      
    // ..
    }
    ?>

    Ich versuche auch, Variablennamen nicht abzukuerzen und pro Begriff auch nur ein Wort zu verwenden, dass sich durch die ganze Applikation zieht (Tabelle-, Klasse-, Variablen-, Parameternamen). Wenn es also in der Datenbank eine Category gibt, finde ich es sinnvoll, die URL-Parameter auch "category_id" und die Objekte "objCategory" zu benennen. Wenn man anfaengt das ganze abzukuerzen ($objCat, $objCategory, "catid", "id", ..) ist das meiner Meinung nach der Anfang von Chaos. Ich fahre damit ziemlich gut. Aber das nur am Rande.


    Ein Rueckwaertszugriff ist mit diesen Klassen also auch moeglich, also wenn ihr zu einem "Article" auch noch die "Category" braucht:


    article_view.php
    PHP-Code:
    <?php
    try {
      
    $objArticle Article::Autoload();
      
    // check
      
    echo $objArticle->Category->Name// Ausgabe des Category Name
    // ..
    ?>

    Natuerlich koennt ihr auch diese Objekte wieder wie die normalen Objekte verwenden, also Save() oder Load*() Operationen darauf ausfuehren.


    Lasst euch nicht abschrecken von einem Framework, probiert es mal aus, stutzt euch die schlechte Ordner- und Konfigurations-Struktur von QCodo etwas zusammen und ihr werdet eine ganze Menge neuer Erfahrungen in OO-Programmierung sammeln. Ich hab dadurch zumindest gemerkt, was Abstraktion wirklich heisst, ich programmiere natuerlich immernoch viel Kleinklein, aber zumindest ums Datenbanking und das leidige SELECT/UPDATE/INSERT muss ich mich nicht mehr kuemmern.


    Das wars vorerst zu diesem Tutorial, ihr seht, es ist eigentlich nicht viel, aber letztlich doch ein ganze Menge, was einem da an Arbeit abgenommen wird.


    Nochmal am Ende sei auf die Examples auf der QCodo-Website hingewiesen, mit der Quellcode-Betrachtung wirklich ne runde Sache und das tolle ist ja, man versteht sofort was der Code macht und genau das sollte ja auch das Ziel sein. Fragen dazu? Einfach hier stellen, wuerde mich ueber einen Erfahrungsaustausch oder eine Diskussion freuen.


    http://www.qcodo.com/
    http://examples.qcodo.com/examples/
    http://www.qcodo.com/downloads/

    http://www.zcodo.com
    (neue abtruennige Version, da der alte Maintainer als einziger mit Zugriff aufs SVN zu langsam auf Aenderungen/Erweiterungen reagiert hat)


    Anhang:
    dump.sql
    Code:
    -- phpMyAdmin SQL Dump
    -- version 2.11.4
    -- http://www.phpmyadmin.net
    --
    -- Host: localhost:3306
    -- Generation Time: May 07, 2008 at 11:25 PM
    -- Server version: 5.0.51
    -- PHP Version: 5.2.5
    
    SET FOREIGN_KEY_CHECKS=0;
    
    SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
    
    SET AUTOCOMMIT=0;
    START TRANSACTION;
    
    --
    -- Database: `phpfriend`
    --
    
    -- --------------------------------------------------------
    
    --
    -- Table structure for table `article`
    --
    
    CREATE TABLE IF NOT EXISTS `article` (
      `id` int(10) unsigned NOT NULL auto_increment,
      `category_id` int(10) unsigned default NULL,
      `name` varchar(50) NOT NULL default '',
      `description` text NOT NULL,
      `price` decimal(9,2) NOT NULL default '0.00',
      PRIMARY KEY  (`id`),
      KEY `category_id` (`category_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
    
    -- --------------------------------------------------------
    
    --
    -- Table structure for table `billing`
    --
    
    CREATE TABLE IF NOT EXISTS `billing` (
      `id` int(10) unsigned NOT NULL auto_increment,
      `cart_id` int(10) unsigned NOT NULL default '0',
      `address` text NOT NULL,
      `creditcard` text NOT NULL,
      `last_update` timestamp NOT NULL default CURRENT_TIMESTAMP,
      PRIMARY KEY  (`id`),
      KEY `cart_id` (`cart_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
    
    -- --------------------------------------------------------
    
    --
    -- Table structure for table `cart`
    --
    
    CREATE TABLE IF NOT EXISTS `cart` (
      `id` int(10) unsigned NOT NULL auto_increment,
      `user_id` int(10) unsigned NOT NULL default '0',
      `total` decimal(9,2) NOT NULL default '0.00',
      `last_update` timestamp NOT NULL default CURRENT_TIMESTAMP,
      PRIMARY KEY  (`id`),
      KEY `user_id` (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
    
    -- --------------------------------------------------------
    
    --
    -- Table structure for table `cart_ref`
    --
    
    CREATE TABLE IF NOT EXISTS `cart_ref` (
      `id` int(10) unsigned NOT NULL auto_increment,
      `cart_id` int(10) unsigned NOT NULL default '0',
      `article_id` int(10) unsigned NOT NULL default '0',
      `quantity` int(10) unsigned NOT NULL default '0',
      PRIMARY KEY  (`id`),
      KEY `cart_id` (`cart_id`),
      KEY `article_id` (`article_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
    
    -- --------------------------------------------------------
    
    --
    -- Table structure for table `category`
    --
    
    CREATE TABLE IF NOT EXISTS `category` (
      `id` int(10) unsigned NOT NULL auto_increment,
      `name` varchar(50) NOT NULL default '',
      PRIMARY KEY  (`id`),
      UNIQUE KEY `name` (`name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
    
    -- --------------------------------------------------------
    
    --
    -- Table structure for table `user`
    --
    
    CREATE TABLE IF NOT EXISTS `user` (
      `id` int(10) unsigned NOT NULL auto_increment,
      `name` varchar(50) NOT NULL default '',
      `email` varchar(100) NOT NULL default '',
      `password` varchar(32) NOT NULL default '',
      `last_update` timestamp NOT NULL default CURRENT_TIMESTAMP,
      PRIMARY KEY  (`id`),
      UNIQUE KEY `name` (`name`),
      UNIQUE KEY `email` (`email`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
    
    --
    -- Constraints for dumped tables
    --
    
    --
    -- Constraints for table `article`
    --
    ALTER TABLE `article`
      ADD CONSTRAINT `article_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
    
    --
    -- Constraints for table `billing`
    --
    ALTER TABLE `billing`
      ADD CONSTRAINT `billing_ibfk_1` FOREIGN KEY (`cart_id`) REFERENCES `cart` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
    
    --
    -- Constraints for table `cart`
    --
    ALTER TABLE `cart`
      ADD CONSTRAINT `cart_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
    
    --
    -- Constraints for table `cart_ref`
    --
    ALTER TABLE `cart_ref`
      ADD CONSTRAINT `cart_ref_ibfk_2` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
      ADD CONSTRAINT `cart_ref_ibfk_1` FOREIGN KEY (`cart_id`) REFERENCES `cart` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
    
    SET FOREIGN_KEY_CHECKS=1;
    
    COMMIT;

  • #2
    Sieht aus als wäre es mal einen Versuch/Blick wert.


    Allerdings bin ich momentan auch mit CakePHP sehr glücklich was sich recht ähnlich bedienen lässt:

    Aus einem gegebenen DB-Schema kann man sich automatisch die Models generieren lassen. Die Beziehungen werden dabei einfach über Namenskonventionen ausgelesen (ein Feld user_id in einer tabelle referenziert auf die Tabelle User und das Feld id...) - somit ist nicht zwingend InnoDb nötig.

    und auch das auslesen ist easy:

    Code:
    //$thi->Article wird einem automatisch im Controller zur Verfügung gestellt..
    $article = $this->Article->findById(1);
    Dabei werden dann evtl. anhängende Dinge wie die Kategorie mitgeladen...:
    Code:
    echo $article['Category']['name']; //Kategorienname
    echo $article['Article']['name'];//Artikelname
    Im Gegensatz zu qcodo werden also Arrays und nicht Objekte zurückgegeben. Das war anfangs ziemlich gewöhnungsbedürftig. Ein Pluspunkt für Qcodo.


    Mal schauen ob ich Zeif dinde mir Qcodo mal etwas genauer anzusehen. Gibt's solche Dinge wie automatische Validierung wenn ich ->save() aufrufe (anhand vorher angegeneber Kriterien)?
    [URL="https://www.quizshow.io/"]Create your own quiz show.[/URL]

    Kommentar


    • #3
      Zitat von Agrajag
      Allerdings bin ich momentan auch mit CakePHP sehr glücklich was sich recht ähnlich bedienen lässt:

      Aus einem gegebenen DB-Schema kann man sich automatisch die Models generieren lassen. Die Beziehungen werden dabei einfach über Namenskonventionen ausgelesen (ein Feld user_id in einer tabelle referenziert auf die Tabelle User und das Feld id...) - somit ist nicht zwingend InnoDb nötig.
      Das haette ich mir fuer QCodo auch gewuenscht.



      Gibt's solche Dinge wie automatische Validierung wenn ich ->save() aufrufe (anhand vorher angegeneber Kriterien)?
      Ja, deshalb muss man wirklich drauf achten, dass das DB-Schema richtig ist. Man bekommt z.B. fuer obiges Beispiel mit folgendem Code diese Exceptions:

      PHP-Code:
      <?php
      try {
          require 
      "includes/prepend.inc.php";
          
      $objArticle = new Article();
          
      $objArticle->Name "My Article"// dieser und die naechsten Werte ungesetzt, 1. Exception
          
      $objArticle->Description "My Description"// dieser und der naechste Wert ungesetzt, 2. Exception
          
      $objArticle->Price "My Price"// dieser Wert ungesetzt, 3. Exception 
          
      $objArticle->Save();
      } catch (
      Exception $objExc) {
          echo 
      $objExc;
      }
      ?>
      Code:
      exception 'QMySqliDatabaseException' with message 'MySqli Error: Column 'name' cannot be null'
      Code:
      exception 'QMySqliDatabaseException' with message 'MySqli Error: Column 'description' cannot be null'
      Code:
      exception 'QInvalidCastException' with message 'Unable to cast string value to double: My Price'

      Schoener faende ich es, wenn die Exception beim Setzen geworfen werden wuerde, so kann man nur schwer unterscheiden, was nun den Fehler verursacht hat, da soweit ich weiss auch kein unterschiedlicher Exception-Code verwendet wird (dafuer ist er ja grade da).


      Trotzdem machts echt Spass damit zu programmieren, kanns einfach nur jedem empfehlen. Ob sich jetzt ein Umstieg von CakePHP zu QCodo lohnt weiss ich natuerlich nicht.

      Kommentar

      Lädt...
      X