Monatliches Archiv: Juni 2013

Datei Upload mit einem Yii Active Record

Datei Upload mit einem Yii Active Record

 

Ein Datei Upload über ein Active Form in Yii zu realisieren, ist gar nicht so schwer, wie man zunächst annimmt. In diesem Artikel möchte ich eine kleine Anleitung bieten, wie man so etwas realisieren könnte. Dabei greife ich zunächst nicht auf weitere Bibliotheken etc zurück, sondern realisiere so etwas mit den Bordmittel von Yii.

Grundsätzlich muss ein Formular für einen Datei Upload ein “enctype=’multipart/form-data’” enthalten, da sonst Dateien vom Browser erst gar nicht übermittelt werden. Dies können wir bei einem CActiveForm wie folgt realisieren:

1
2
3
4
5
<?php $form=$this->beginWidget('CActiveForm',array(
    'id'=>'app-gutschein-form',
    'enableAjaxValidation'=>false,
    'htmlOptions' => array('enctype' => 'multipart/form-data'),
));

Desweiteren müssen wir dann noch das entsprechende Feld zu einem Datei-Feld ändern.

1
2
3
4
5
6
<div class="row">
  <?php echo $form->labelEx($model,'banner'); ?>
  <?php echo $form->fileField($model,'banner'); ?>
  <?php echo $form->error($model,'banner'); ?>
  <?php echo CHtml::image(Yii::app()->request->baseUrl.'/images/uploads/'.$model->banner,"Bild",array("width"=>200)); ?>
</div>

Da in jedem ActiveRecord in Yii entsprechende Regeln für die Datenübermittlung notwendig sind, müssen wir auch für unsere Datei-Felder hier noch entsprechende regeln definieren. Im Rules Part fügen wir also noch folgende Array’s hinzu:

1
2
array('banner', 'file','types'=>'jpg, gif, png','allowEmpty'=>false,'on'=>'update'),
array('banner', 'length', 'max'=>255, 'on'=>'insert,update'),

Nun müssen wir uns um den Datei-Upload im Yii Controller kümmern. Wir gehen beispielsweise in die actionUpdate(), falls der Dateiupload an dieser Stelle eingebaut werden soll. Hier fragen wir erst den alten Namen des Banners ab und reichen anschließend per Massenzuweisung alle Daten des Formulars an das ActiveRecord weiter. Nun holen wir uns die Instanz des hochgeladenen Bildes über CUploadedFile. Wenn kein Bild hochgeladen wurde, gibt es auch keinen Dateinamen hierzu, also weisen wir hier an, dass das ActiveRecord den bisherigen Banner-Namen beibehalten sol,l anderenfalls möchten wir den Namen aus der ActiveRecord_id, dem String “_banner” und dem bisherigen Dateinamen abspeichern. Nach dem Speichern des ActiveRecord Objektes fragen wir zunächst noch einmal ab, ob eine Datei hochgeladen wurde und Speichern die Datei anschließend in Images/Uploads des Arbeitsverzeichnisses.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if(isset($_POST['ArObject']))
{
  $filename_banner_old = $model->banner;
  $model->attributes=$_POST['ArObject'];
  $uploaded_banner = CUploadedFile::getInstance($model, 'banner');
  if(empty($uploaded_banner)){
    $model->banner = $filename_banner_old;
  }else{
    $filename_banner = $model->id.'_banner'.$uploaded_banner;
    $model->banner = $filename_banner;
  }
  if($model->save()){
    if(!empty($uploaded_banner)){
      $uploaded_banner->saveAs(Yii::app()->basePath.'/../images/uploads/'.$filename_banner);
    }
    $this->redirect("index.php");
  }
}

Hier wurde noch nicht berücksichtigt, wann eine Datei wieder gelöscht wird. Dies muss unter Umständen natürlich auch noch geschehen.

Wenn es noch einen einfacheren Weg gibt, eine Datei hochzuladen, so lasst es mich wissen.

Yii Bootstrap

Das Yii Bootstrap von Christoffer Niska macht es auf einfache Weise möglich, das Twitter Bootstrap in Yii Projekten zu verwenden. Das Twitter Bootstrap ist eine Sammlung von CSS Befehlen, die schon ein recht gutes Layout vorgeben. Es gab eine Zeit lang, da hat sich das Twitter Bootstrap ohne Ende verbreitet und jeder wollte es verwenden. Auch heute gibt es immer mehr Anhänger des Twitter Bootstrap. Was im Twitter Bootstrap unabhängig von dem Yii Bootstrap enthalten ist, kann man auf der Twitter Bootstrap Webseite sehen.

Das Yii Bootstrap kann von der Webseite heruntergeladen werden. Anschließend wird das Zip-Archiv entpackt und in das Verzeichnis /protected/extensions/bootstrap verschoben. Wenn man die Standard Themes des Yii Bootstrap verwenden möchte, so kann man das Theme Verzeichnis in das /themes Verzeichnis von Yii kopiert werden.

Da das Yii Bootstrap auch schon viele vorgefertigte Yii-Widgets mitliefert, muss in der Konfiguration natürlich noch etwas hinzugefügt werden:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Yii::setPathOfAlias('bootstrap', dirname(__FILE__).'/../extensions/bootstrap');
 
return array(
    'theme'=>'bootstrap', // requires you to copy the theme under your themes directory
    'modules'=>array(
        'gii'=>array(
            'generatorPaths'=>array(
                'bootstrap.gii',
            ),
        ),
    ),
    'components'=>array(
        'bootstrap'=>array(
            'class'=>'bootstrap.components.Bootstrap',
        ),
    ),
);

 

Wichtig ist hier die erste Zeile mit der der Pfad für die Bootstrap Widgets gesetzt werden.

 

Darüber hinaus bietet Yii Bootstrap auch noch einen Gii-Crud-Generator an,der ebenfalls mit obenstehender Konfiguration eingebunden wird. Wenn wir uns jetzt in unser Gii einloggen, finden wir als untersten Menüpunkt den Yii Bootstrap Gii Generator. Wir können also, wie wir es von Yii schon gewohnt sind, auch mit Gii uns Code generieren lassen, der anschließend mit Yii Bootstrap Widgets ausgeliefert wird.

Im Layout muss das Yii Bootstrap mit folgendem Befehl eingebunden werden:

 

1
Yii::app()->bootstrap->register();

 

Wenn wir jetzt unsere Stylesheet Dateien ganz normal einbinden, wie wir es gewohnt sind, werden wir feststellen, dass das Yii Bootstrap nach unseren fest eingebundenen Stylesheet Dateien eingebunden wird. Dies ist allerdings manchmal nicht ganz sinnvoll, da man ja nicht immer genau den Style des Twitter Bootstrap haben möchte, sondern auf die Formatierung in der Webseite selbst Hand anlegen möchte. Hier habe ich folgenden Weg gefunden, um das Yii Bootstrap einzubinden und doch anschließend noch meine eigene CSS-Datei:

 

1
2
3
4
Yii::app()->bootstrap->register();
$baseUrl = Yii::app()->baseUrl;
$cs = Yii::app()->getClientScript();
$cs->registerCssFile($baseUrl.'/css/style.css');

 

Wenn ich mein Stylesheet nach diesem Schema einbinde, habe ich keine Probleme mehr und kann in meinem Stylesheet nun die Styles des Yii Bootstrap überschreiben.

 

Wer hat das Yii Bootstrap schon einmal eingesetzt und kennt es schon bzw. hat damit schon Erfahrungen gemacht?

 

E-Mails via SMTP mit Yii versenden

Wenn ich Mails mit einem System wie Yii versenden möchte, mache ich das nicht mit der PHP-Mail Funktion. Der Aufwand, der betrieben werden muss, um eine Mail mit der PHP Funktion zu versenden, ohne dass diese ein hohes Spam-Rating bekommt, ist mir zu hoch.

Nun habe ich mich auf die Suche gemacht nach einer Lösung, mit der der Aufwand nicht so hoch ist, eine E-Mail via SMTP zu versenden. Da ich jedoch auf allen Servern eine Authentifizierung vor den SMTP-Dienst geschaltet habe, ist es notwendig sich bei diesem erst zu Authentifizieren. Ich habe im Erweiterungsverzeichnis von Yii die Extension EMailer gefunden. Mit dieser Erweiterung ist ganz einfach E-Mails via SMTP zu versenden. Die Klasse bzw. Erweiterung stellt nichts weiter nach, als einen Yii-Wrapper für PhpMailer. Wer mit PHP schon mal versucht hat, sich mit dem Thema E-Mail versenden auseinanderzusetzen, ist zwangsläufig schon auf den PhpMailer gestossen.

Um die Erweiterung zu installieren, wird einfach die Zip-Datei heruntergeladen und im Verzeichnis /protected/extensions/mailer kopiert. Anschließend müssen noch folgende Einträge in die Konfigurationsdatei geschrieben werden:

1
2
3
4
5
6
7
8
9
10
11
12
13
$mailer = Yii::app()->mailer;
$mailer->CharSet = 'utf-8';
$mailer->IsSMTP();
$mailer->SMTPAuth = true;
$mailer->Host = 'smtp.domain.tld';
$mailer->Username = 'user@domain.tld';
$mailer->Password = 'secure';
$mailer->From = 'info@domain.tld;
$mailer->FromName = '
Absendername';
$mailer->AddAddress('
E-Mail Adresse');
$mailer->Subject = '
Betreff';
$mailer->Body = '
E-Mail Text';
$mailer->Send();

So wird mit einem relativ kleinen Code in Yii vernünftig eine E-Mail versendet. Für mich ist der Aufwand, eine Mail via Yii zu versenden, so erheblich geringer, zudem die Spam-Gefahr durch ein Postfach wesentlich geringer ist, wie wenn diese mit der PHP mail() Funktion versendet wird.

Was meint Ihr dazu?

Yii CGridView Spalte mit CLinkColumn verlinken

CLinkColumn erleichtert die Arbeit eine Zelle zu verlinken

Im Yii CGridView ist es relativ einfach eine Spalte zu verlinken. Nehmen wir mal als Beispiel an, es gibt eine Spalte in der Lieferanten aufgelistet werden sollen. Viele der Lieferanten besitzen eine Webseite und diese soll im CGridView so angezeigt werden, dass die Webseite direkt verlinkt ist. Yii bringt hierfür die Klasse CLinkColumn mit. Mit der Klasse CLinkColumn können wir eine Spalte direkt verlinken. Eingesetzt wird CLinkColumn wie in dem folgenden Beispiel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$this->widget('bootstrap.widgets.CGridView', array(
  'id'=>'kunden-grid',
  'dataProvider'=>$model->search(),
  'filter'=>$model,
  'summaryText'=>'',
  'columns'=>array(
    'id',
    array(
      'class'=>'LinkColumn',
      'header'=>'$data->firma',
      'labelExpression'=>'$data->firma',
      'urlExpression'=>'$data->website',
      'linkHtmlOptions'=>array('target'=>'_blank'),
    ),
    'strasse',
    'plz',
    'ort',
  ),
));

Wir sehen, dass wir also als Spalte ein Array angeben und diesem die Klasse CLinkColumnübergeben. Das ganze hat jedoch einen gravierenden Nachteil, der mir gar nicht gefällt. Ich kann hier keinen Namen einer Eigenschaft eines ActiveRecord übergeben. Dies stört mich aus dem einfachen Grund, dass es mit dem CLinkColumn nicht mehr so einfach wird, Filter-Optionen mitzugeben. Ich bekomme also nicht einfach ohne weiteres eine Filterzelle angezeigt. Außerdem möchte ich, dass die Spaltenbeschriftung genau mit dem ActiveRecord übereinstimmt. Ich habe hierzu eine eigene Klasse abgeleitet, die ich einfach unter “components” abgelegt habe. Die Klasse habe ich LinkColumn genannt und baut sich wie folgt auf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class LinkColumn extends CLinkColumn{
  public $filter;

  public $name = null;
  public $sortable=true;

  public function renderFilterCellContent(){
    if(is_string($this->filter))
      echo '<div class="filter-container">'.$this->filter.'</div>';
    else if($this->filter!==false && $this->grid->filter!==null && $this->name!==null && strpos($this->name,'.')===false)
    {
      if(is_array($this->filter))
        echo '<div class="filter-container">'.CHtml::activeDropDownList($this->grid->filter, $this->name, $this->filter, array('id'=>false,'prompt'=>'')).'</div>';
      else if($this->filter===null)
        echo '<div class="filter-container">'.CHtml::activeTextField($this->grid->filter, $this->name, array('id'=>false)).'</div>';
    }
    else
      parent::renderFilterCellContent();
  }

  public function renderHeaderCellContent(){
    if($this->grid->enableSorting && $this->sortable && $this->name!==null)
      echo $this->grid->dataProvider->getSort()->link($this->name,$this->header,array('class'=>'sort-link'));
    else if($this->name!==null && $this->header===null)
    {
      if($this->grid->dataProvider instanceof CActiveDataProvider)
        echo CHtml::encode($this->grid->dataProvider->model->getAttributeLabel($this->name));
      else
        echo CHtml::encode($this->name);
    }
    else
      parent::renderHeaderCellContent();
  }
}

Dies mag im ersten Moment sehr verwirrend aussehen. Doch was habe ich genau getan? Ich habe eigentlich hier nichts weiter gemacht, als eine neue Klasse mit dem Namen LinkColumn von der Klasse CLinkColumn abgeleitet und habe drei Eigenschaften (filter, name, sortable) definiert. Hinzu habe ich noch zwei Methoden (renderFilterCellContent und renderHeaderCellContent) aus der Klasse CDataColumn kopiert. Das Ergebnis was daraus resultiert, ist eine Möglichkeit mit der es mir möglich wird, einen Zelleninhalt zu verlinken unter Angabe eines eigenen Ziels und der Verwendung einer Eigenschaft aus dem CActiveRecord. Dies sieht dann folgendermaßen aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$this->widget('bootstrap.widgets.CGridView', array(
  'id'=>'kunden-grid',
  'type'=>'striped bordered condensed',
  'dataProvider'=>$model->search(),
  'filter'=>$model,
  'summaryText'=>'',
  'columns'=>array(
    'id',
    array(
      'class'=>'LinkColumn',
      'name'=>'firma',
      'labelExpression'=>'$data->firma',
      'urlExpression'=>'array("view", "id"=>$data->id)',
    ),
    'strasse',
    'plz',
    'ort',
  ),
));

Ich habe diesen ganzen Aufwand eigentlich nur betrieben, um hier noch weiter arbeiten zu können. Hier ging es mir nicht mehr um die Verlinkung einer Externen Seite, sondern um Asyncron daten vom Server holen zu können. Ich suchte eine Möglichkeit, Ajax Links zu generieren, die auch nach dem Pager noch funktionieren. Das Problem mit dem standard Yii Gridview ist, dass ich zwar Ajax Links via CHTML::ajaxLink() auch in eine Zelle generieren kann. Da jedoch das Gridview je Filterauswahl oder Pager Seitenauswahl via Ajax geupdated wird, funktionieren die Ajax Links nicht mehr. Hier musste eine andere Lösung her. Mit meiner neuen LinkColumn Klasse, ist es ganz einfach möglich, eigene Ajax Links zu generieren, die vollkommen unabhängig von Yii sind. Mit folgendem Code generiere ich meine Ajax Links:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$this->widget('bootstrap.widgets.CGridView', array(
  'id'=>'kunden-grid',
  'type'=>'striped bordered condensed',
  'dataProvider'=>$model->search(),
  'filter'=>$model,
  'summaryText'=>'',
  'columns'=>array(
    'id',
    array(
      'class'=>'LinkColumn',
      'name'=>'notice',
      'labelExpression'=>'Notiz',
      'urlExpression'=>'array("view", "id"=>$data->id)',
      'linkHtmlOptions'=>array('onclick'=>'js:$.ajax({url: $(this).attr("href"),success: function(data){$("#target_id").html(data);}}); return false;'),
    ),
    'strasse',
    'plz',
    'ort',
  ),
));

Was passiert hier genau? Nun ich füge eigentlich nur noch dem verlinkenden A-Tag ein weiteres Attribut “onclick” hinzu und fülle dieses mit einem jQuery Kommando. Vorraussetzung hierfür ist natürlich, dass jQuery standardmäßig auf dieser Seite eingebunden ist. Mit dem jQuery Befehl, führe ich eine Asyncrone Abfrage an den Server durch und kann mir die zurückgelieferten Inhalte hier in einem HTML-Element ausgeben lassen.