Refaktorisierung mit Symfony (Teil 1/5)

Im offiziellen Symfony-Blog wird derzeit eine fünfteilige Artikelserie über das Thema Refaktorisierung (Refactoring) veröffentlicht. Da die Inhalte sehr interessant sind, habe ich mich entschieden eine deutsche Version der Artikel zu entwerfen.

Dies ist also Teil 1 von “Refaktorisierung mit Symfony”, eine freie, deutsche Übersetzung und leicht abgewandelte Version des Originalartikels “Call the expert: A refactoring story (Part 1/5)” veröffentlicht im offziellen Symfony-Blog.

Wikipedia über Refaktorisierung:

“Refactoring (deutsch auch Refaktorisierung, Refaktorierung oder schlicht Umgestaltung) bezeichnet in der Software-Entwicklung die manuelle oder automatisierte Strukturverbesserung von Programm-Quelltexten unter Beibehaltung des beobachtbaren Programm-Verhaltens. Dabei sollen die Lesbarkeit, Verständlichkeit, Wartbarkeit und Erweiterbarkeit verbessert werden, mit dem Ziel, den jeweiligen Aufwand für Fehleranalyse und funktionale Erweiterungen deutlich zu senken.”

Die Geschichte

Vor einiger Zeit fragte Vince, ein erfahrener PHP-Entwickler, den Symfony-Entwickler Fabien Potencier, ob er einen Blick auf sein erstes Symfony-Projekt, ein Onlineshop, werfen könnte.

Sein Kunde war sehr glücklich, dass die Webseite problemlos lief. Auch Vince war zufrieden, da er die Webseite pünktlich fertigstellte. Aber er war sich nicht so sicher über die Qualität seines Codes.

Also schrieb er Fabien an, um Feedback zu der Qualität seines Codes zu bekommen. Statt ihm nur per E-Mail zu antworten, lud ihn Fabien zu Sensio Labs ein um an einer echten Refactoring-Session teilzunehmen.

Da manche seiner Fehler recht häufig in Projekten passieren, hat sich Fabien entschieden den Ablauf des Refactoring im offiziellen Symfony-Blog wiederzugeben, in der Hoffnung, das andere Symfony-Entwickler ebenso davon lernen können, wie es Vince getan hat.

Heute, in Teil 1, beschreiben wir ein paar Features der Webseite und schauen uns ein paar kurze Codeabschnitte an. Der eigentliche Refacorting-Prozess beginnt dann in Teil 2.

Das Projekt wurde mit Symfony 1.1 entwickelt.

Das Schema

Das Datenbankschema besteht aus zwei Tabellen, einer Produkt-Tabelle (product) und einer Kategorie-Tabelle (category):

# config/schema.yml
propel:
  product:
    title:       { type: varchar(255), required: true }
    image:       varchar(255)
    description: longvarchar
    price:       { type: decimal, required: true }
    is_new:      boolean
    is_in_stock: boolean
    created_at:  timestamp
    category_id: ~
 
  category:
    name: varchar(255)

Die Startseite

Die Startseite der Webseite besteht aus einer Liste aller verfügbaren Produkte (is_in_stock auf true gesetzt).

Die Startseite

Alle Features, die wir später refaktorisieren, werden vom Produktmodul (product) gesteuert.

Hier ist die index-Action, welche die Startseite steuert:

// apps/frontend/modules/product/actions/actions.class.php
public function executeIndex()
{
  $criteria = new Criteria();
  $criteria->add(ProductPeer::IS_IN_STOCK, true);
  $criteria->addAscendingOrderByColumn(ProductPeer::PRICE);
  $criteria->setLimit(10);
 
  $this->products = ProductPeer::doSelectJoinCategory($criteria);
 
  $this->getResponse()->setTitle('Alle Produkte');
  $this->getResponse()->addStylesheet('homepage.css');
 
  return sfView::SUCCESS;
}

Und das dazugehörige Template indexSucess.php:

 
<h1>Unsere Produkte</h1>
 
<?php foreach ($products as $product): ?>
  <div>
    <h2>
      <?php echo $product->getTitle() ?>
      <?php if ($product->getIsNew()): ?><span style="margin-left: 10px; color: #e55">NEU!</span><?php endif; ?>
    </h2>
    <div style="margin-bottom: 10px">
      <em>Kategorie</em>: <?php echo $product->getCategory()->getName() ?> -
      <em>Preis</em>: Only $<?php echo $product->getPrice() ?> - 
      <?php if (in_array($product->getId(), array_keys($sf_user->getAttribute('favorites', array())))): ?>
        <a href="<?php echo url_for('product/removeFromFavorites?id='.$product->getId()) ?>"><img src="/images/favorite.png" /></a>
      <?php else: ?>
        <small><?php echo link_to('Zu den Favoriten hinzufügen', 'product/addToFavorites?id='.$product->getId()) ?></small>
      <?php endif; ?>
    </div>
    <div>
      <div style="float: left">
        <img width="100px" src="/images/products/<?php echo $product->getImage() ?>" />
      </div>
      <p>
        <?php echo $product->getDescription() ?>
        <?php if ($sf_user->isAuthenticated()): ?>
          <p style="text-align: right"><a href="<?php echo url_for('product/edit?id='.$product->getId()) ?>">Produkt editieren</a></p>
        <?php endif; ?>
      </p>
      <br style="clear: both" />
    </div>
    <div style="text-align: right">
      <?php echo link_to(image_tag('/images/add_to_cart.png'), 'product/buy?id='.$product->getId()) ?>
    </div>
    <hr />
  </div>
<?php endforeach; ?>

Die Favoriten

Wie du eventuell schon auf dem Screenshot gesehen hast, kann der Benutzer Produkte zu seinen Favoriten hinzufügen. Das Feature ist recht einfach: Man klickt auf den “Zu den Favoriten hinzufügen”-Link um ein Produkt seinen Favoriten hinzuzufügen und man klickt auf das Sternsymbol um es wieder zu entfernen.

Die Favoriten werden von den Methoden executeAddToFacorites() und executeRemoveFromFavorites() des Produktmoduls verarbeitet:

public function executeAddToFavorites()
{
  $product = ProductPeer::retrieveByPk($this->getRequestParameter('id'));
  $this->forward404Unless($product);
 
  $favorites = $this->getUser()->getAttribute('favorites');
  $favorites[$product->getId()] = true;
 
  $this->getUser()->setAttribute('favorites', $favorites);
 
  $this->redirect('product/index');
}
 
public function executeRemoveFromFavorites()
{
  $product = ProductPeer::retrieveByPk($this->getRequestParameter('id'));
  $this->forward404Unless($product);
 
  $favorites = $this->getUser()->getAttribute('favorites');
  unset($favorites[$product->getId()]);
 
  $this->getUser()->setAttribute('favorites', $favorites);
 
  $this->redirect('product/index');
}

Produkte editieren

Als eingeloggter Administrator können die Produktinformationen auch editiert werden.

Produkt editieren

Diese Seite wird von der edit-Action verarbeitet:

public function executeEdit()
{
  $this->product = ProductPeer::retrieveByPk($this->getRequestParameter('id'));
  if (is_null($this->product))
  {
    $this->forward404();
  }
 
  $this->form = new ProductForm($this->product);
 
  if (sfRequest::POST == $this->getRequest()->getMethod())
  {
    $this->form->bind($this->getRequestParameter('product'), $this->getRequest()->getFiles('product'));
    if ($this->form->isValid())
    {
      // aktuelles produkt holen
      $product = $this->form->getObject();
      $image = $this->form->getValue('image');
      $currentImage = $product->getImage();
 
      if ($image)
      {
        // altes bild entfernen
        if (file_exists($file = sfConfig::get('sf_web_dir').'/images/products/'.$currentImage))
        {
          unlink($file);
        }
      }
 
      // produkt aktualisieren
      $product = $this->form->updateObject();
 
      if ($image)
      {
        // bild speichern
        $filename = sha1($image->getOriginalName()).$image->getExtension($image->getOriginalExtension());
        $image->save(sfConfig::get('sf_web_dir').'/images/products/'.$filename);
 
        $product->setImage($filename);
      }
      else
      {
        $product->setImage($currentImage);
      }
 
      // produkt speichern
      $product->save();
 
      $this->getUser()->setFlash('notice', sprintf('Das Produkt "%s" wurde erfolgreich aktualisiert.', $product->getTitle()));
      $this->redirect('product/index');
    }
  }
}

Und das Template editSuccess.php:

<h1>Editieren "<?php echo $product->getTitle() ?>" <small>#<?php echo $product->getId() ?></small></h1>
 
<p><?php echo link_to('Zurück zur Homepage', '@homepage') ?></p>
 
<form action="<?php echo url_for('product/edit?id='.$product->getId()) ?>" method="POST" enctype="multipart/form-data">
  <table cellspacing="0">
    <?php echo $form ?>
    <tr>
      <td colspan="2" style="text-align: right"><input type="submit" value="Speichern" /></td>
    </tr>
  </table>
</form>

Dies ist alles für Heute. Der eigentliche Refactoring-Prozess beginnt im nächsten Teil. In der Zwischenzeit nimm dir die Zeit den Code zu lesen und überleg dir, was du refaktorisieren würdest, wie und warum.

http%3A%2F%2Fwww.anti-hype.de%2F2008%2F09%2F20%2Frefaktorisierung-mit-symfony-teil-15& layout=standard&show-faces=true&width=500& action=like&colorscheme=light" scrolling="no" frameborder="0" allowTransparency="true" style="border:none; overflow:hidden; width:500px; height:60px">

Kommentare (2)

[...] Dies ist der zweite Teil von “Refaktorisierung mit Symfony”, eine freie, deutsche Übersetzung und leicht abgewandelte Version des Originalartikels “Call the expert: A refactoring story (Part 2/5)” veröffentlicht im offziellen Symfony-Blog. Teil 1 der Serie finden sich hier: Refaktorisierung mit Symfony (Teil 1/5). [...]

[...] offiziellen symfony-Blog gemacht. Mittlerweile sind die ersten beiden Teile komplett übersetzt und hier und hier verfügbar. Sehr [...]

Kommentieren?

Dein Kommentar