Schema

i campi di testo devono essere definiti del tipo TEXT(65000 bytes) che esporta in database.yml come "clob" che non da problemi di validazione

Controller

public function executeListimages(sfWebRequest $request)  {
    
// parametri dal controller
    $id $request->getParameter('id');

    
// qry
    $this->car_images =
        
Doctrine_Query::create()
          ->
select('*')
          ->
from('CarImage')
          ->
where('car_image_id = ?'$id)
          ->
execute();


    
$q $this->createQuery()
            ->
select('c.name, j.title')
            ->
from('Category c')
            ->
leftJoin('c.Job j')
            ->
where('c.level = ?',1)
            ->
where('j.expires_at > ?',date'Y-m-d h:i:s'time() ))
            ->
setHydrationMode(Doctrine::HYDRATE_ARRAY);


    
// relations
    $q Doctrine_Query::create()
        ->
from('User u')
        ->
leftJoin('u.Phonenumbers p')
        ->
leftJoin('u.Groups g');

    
$users $q->execute();

        
// controlla la sintassi con
        echo $q->getSqlQuery();
}

// accedere all'internazionalizzazione, fuori dal template
//$this->getContext() or
sfContext::getInstance()->getI18N()->__($text$args'messages');
// da action
$culture $this->getUser()->getCulture();

// la form va configurata per mostrare el form relative alle lingue
class HorsesForm extends BaseHorsesForm {
    public function 
configure() {
        
$this->embedI18n(array('en''it'));
    }
}
/*
//generator.yml
generator:
  class: sfPropelGenerator # solo questo generatore funziona propel:generate-admin! init-admin è il vecchio generator e non funziona!
      form:
        # le form embedded devono essere chiamate esplicitamente
        display:
          Lingue: [ en, it ]
          Immagini: [_images]
*/
//in action.php
$this->horses_list HorsesPeer::doSelectWithI18n($c'it');
// nel template
echo $horse->getSex('en'); // tradotto in una lingua
echo $horse->getSex(); // nella lingua del utente

aggiungere una action senza modificare il plugin

// file: addExpirationAction.class.php
class addExpirationAction extends sfAction {
    function 
execute() {}
}

caricare helpers fuori dalle view

 
public function preExecute(){
    // sf1.4
    sfProjectConfiguration::getActive()->loadHelpers(array('Url','Tag','I18N','Asset','Date','Mail'));
}

rendere parti di HTML o altro(es. body email, JSON, PDF) con template PHP dal controller

controller{
  method() {
    $this->getPartial('partial_name', array('var'=>'value'));
  }
}

rendere dati JSON

public function executeListjson(sfWebRequest $request){

        
$param $request->getGetParameter('query''');

        
$objects Doctrine_Query::create()
        ->
select('m.*')
        ->
from('Model m')
        ->
where("m.name LIKE ? ""%$param%")
        ->
addOrderBy('name')
        ->
limit(50)
        ->
execute();

        
$key_field 'name';
        
$val_field 'model_id';
        
$total_count 0;
        
$pos 0;
        
$json YUI::objects_to_json($objects$key_field$val_field$total_count $pos);

        
$this->setLayout(false);
        return  
$this->renderText($json);
    }

    
// aggiungere la cache dei dati
    public function executeDisabled(sfWebRequest $request) {
        
$this->setLayout(false);

        
// calcola un id per la richiesta corrente
        $cache_id $this->calcCacheKeyFor($request);

        
// istanzia oggetto file cache
        $cache = new sfFileCache( array( 'cache_dir' => sfConfig::get('sf_cache_dir')."/configurator/" ) );

        
// se esiste una risposta precalcolata
        if ( $cache->has($cache_id) ) {
          
$json '/*cache:'.$cache_id.'*/'.$cache->get($cache_id);
        }else{
          
// algoritmo
          $json $this->calcJSONDisabled($request);

          
$cache->set($cache_id$json7*24*60*60 );// 1 settimana di validità
        }

        
$q Doctrine_Query::create()
            ->
useQueryCache(new Doctrine_Cache_Apc(), 1*24*60*60 );


        return 
$this->renderText($json);
    }

View

// nei template delle form hai a disposizione $form, che ha un l'accesso all'oggetto corrente
  $car $form->getObject();
  
var_dump$car->car_id );
  
var_dump(get_class_methods$this ) );

// e l'oggetto corrente, ad esempio in /car hai $car nella view
// $car ha tutti gli oggetti collegati
$car->get('Model')

// se occorrono altri oggetti, nel controller attaccali a form o all'oggetto corrente

$this->form->CA Doctrine_Query::create()
            ->
select('c.*')
            ->
from('CopyApproximation c')
            ->
execute();

use_helper('I18N')
// usare la cultura corrente per mostrare immagini differenti
echo image_tag($sf_user->getCulture().'/myText.gif')
// gestione dei plurali
echo format_number_choice(
  
'[0]Nobody is logged|[1]There is 1 person logged|(1,+Inf]There are %1% persons logged',
  array(
'%1%' => count_logged()), count_logged())

un autocomplete JSON

use_helper('CIQuickCreate','CIYUI');
$cur_fc sfConfig::get('env')=='prod' '/backend.php' '/backend_dev.php';
echo 
YUI::sf_ajax_autocomplete(
    
$form,
    
'color_id',
    
"$cur_fc/color/listjson",
    
'name',
    
'color_id',
    array(
'url'=>"$cur_fc/color/new?menu=0",'height'=>'600'));

// http://www.symfony-project.org/forms/1_2/en/A-Widgets#chapter_a_choice_widgets
$w = new sfWidgetFormChoice(array(
  
'renderer_class'   => 'sfWidgetFormPropelJQueryAutocompleter',
  
'renderer_options' => array(
    
'model' => 'Article',
    
'url'   => '/autocomplete_script',
  ),
));

Sesione

class mymoduleActions extends sfActions{
    
// Store data in the user session
    $this->getUser()->setAttribute('nickname'$nickname);
    
// Retrieve data from the user session with a default value
    $nickname $this->getUser()->getAttribute('nickname''Anonymous Coward');
    
// altri metodi
    $this->getUser()->getAttributeHolder()->remove('nickname');
    
$this->getUser()->getAttributeHolder()->clear();
}

// nella view
<?php echo $sf_user->getAttribute('nickname'?>

mostrare un campo che è derivato da una tabella collegata

in una adminForm, inserire nel model il getter relativo, quindi in generator.yml è disponibile il campo "brand"

class Car extends BaseCar {
    public function 
getBrand(){
        return 
$this->get('Model')->get('Brand')->get('name');
    }
}

Form

caricare files

richiede plugin:install sfFormExtraPlugin

class ContentForm extends BaseContentForm {
    public function 
configure() {
        
$img_field 'image';
        
$this->widgetSchema[$img_field] = new sfWidgetFormInputFileEditable(array(
            
'label'     => 'Immagine',
            
'file_src'  => $this->getObject()->getImageThubnail(150150),
            
'is_image'  => true,
            
'edit_mode' => !$this->isNew() ,
            
'template'  => '<div class="form-img-upload">%file%<br />%input%<br />%delete% %delete_label%</div>',
        ));
        
/*
         * max_size: The maximum file size
        * mime_types: Allowed mime types array or category (available categories: web_images)
        * mime_type_guessers: An array of mime type guesser PHP callables (must return the mime type or null)
        * mime_categories: An array of mime type categories (web_images is defined by default)
        * path: The path where to save the file - as used by the sfValidatedFile class (optional)
        * validated_file_class: Name of the class that manages the cleaned uploaded file (optional)
         */
        $this->validatorSchema[$img_field] = new sfValidatorFile(array(
            
'required'   => false,
            
'path'       => CI::calcFilePATH($this->getObject(), $img_field),
            
'mime_types' => 'web_images',
            
'max_size' => 65*1000 // 65 KB
        ));

        
$this->validatorSchema[$img_field.'_delete'] = new sfValidatorBoolean();

        
/*  validatori da ricordare:
        $this->setValidators(array(
          'email'   => new sfValidatorEmail(),
          'message' => new sfValidatorString(array('max_length' => 255)),
        )); */
    }
}

TinyMCE

$this->widgetSchema['body'] =  new sfWidgetFormTextarea(array(),array(
      
'rows' => 8,
      
'cols' => 60
    
));

    
/*
    $this->widgetSchema['body'] =  new sfWidgetFormTextareaTinyMCE(
      array(),
      array(
        'class' => 'foo'
        //'theme'  The Tiny MCE theme (advanced by default)
        //width  Width
        //height  Height
        //config  An array of specific JavaScript configuration
        )
    );
    */
    $this->validatorSchema['body'] = new sfValidatorString(array('max_length' => 10000));

upload filmati

$img_field 'movie';
    
$this->widgetSchema[$img_field] = new sfWidgetFormInputFileEditable(array(
        
'label'     => 'Filmato',
        
'file_src'  =>  $this->getObject()->getMovieThumbnailTag(),

        
'edit_mode' => !$this->isNew()  ,
        
'template'  => '<div>%input%<br /> %file% %delete% %delete_label%</div>'
    
));
    
$this->validatorSchema[$img_field] = new sfValidatorFile(array(
        
'required'   => false,
        
'path'       => CI::calcFilePATH($this->getObject(), $img_field),
        
'mime_types' => array(
            
'video/3gpp',
            
'video/dl' ,
            
'video/gl' ,
            
'video/mj2',
            
'video/mpeg',
            
'video/quicktime',
            
'video/vdo',
            
'video/vivo',
            
'video/vnd.fvt' ,
            
'video/vnd.mpegurl',
            
'video/vnd.nokia.interleaved-multimedia' ,
            
'video/vnd.objectvideo' ,
            
'video/vnd.sealed.mpeg1' ,
            
'video/vnd.sealed.mpeg4' ,
            
'video/vnd.sealed.swf',
            
'video/vnd.sealedmedia.softseal.mov' ,
            
'video/vnd.vivo' ,
            
'video/x-fli',
            
'video/x-ms-asf' ,
            
'video/x-ms-wmv' ,
            
'video/x-msvideo' ,
            
'video/x-sgi-movie'
        
),// avi flv mov mpeg ogg wmv
        'max_size' => 36*1000*1000 // 16MB
    ));
    
$this->validatorSchema[$img_field.'_delete'] = new sfValidatorBoolean();

Associazione molti a molti

class ItemForm extends BaseItemForm
{
    public function 
configure()
    {
        
$cultures = array('it''en''fr''de');
        
$this->embedI18n($cultures);
        
$item $this->getObject();


        
$available_note ItemNotePeer::doSelectWithI18n(new Criteria(), 'it');
        
$a_id_note = array();
        foreach(
$available_note as $n){
            
$a_id_note[$n->getID()] = $n->getTitle();
        }

        
$this->widgetSchema['item_has_note_list'] = new sfWidgetFormSelectDoubleList(array(
            
'choices' => $a_id_note,
            
'label_associated' => 'Associate',
            
'label_unassociated' => 'Non Associate',
            
'is_hidden' => $item->isNew(),
            
'label' => 'Note associate'
            
));

        
//$this->validatorSchema->setOption('allow_extra_fields', true);
        // validazione
        $this->validatorSchema['item_has_notes'] = new sfValidatorChoice(array(
          
'choices' => $a_id_note,
          
'required' => false
        
));

        
parent::configure();
    }
}

schema associato necessario

  item_has_note:
    item_id: { type: integer, primaryKey: true, foreignTable: item, foreignReference: id, required: true, onDelete: cascade, onUpdate: cascade }
    note_id: { type: integer, primaryKey: true, foreignTable: item_note, foreignReference: id, required: true, onDelete: cascade, onUpdate: cascade }
  item_note:
    id: ~
  item_note_i18n:
    title: { type: VARCHAR, size: '255', required: false }
    description: { type: LONGVARCHAR, required: false }

molti a molti con autocomplete tag

setup schema

tableName: blog_post
  columns:
    id:
      type: integer
      primary: true
      autoincrement: true
    title:
      type: string(255)
      notnull: true
    content:
      type: clob
      notnull: true
    excerpt:
      type: clob
      notnull: true
  relations:
    Tags:
      class: Tag
      local: blog_post_id
      foreign: tag_id
      type: many
      foreignType: many
      foreignAlias: BlogPosts
      refClass: BlogPostTag
 
BlogPostTag:
  tableName: blog_post_tag
  columns:
    blog_post_id:
      type: integer
      primary: true
    tag_id:
      type: integer
      primary: true
 
Tag:
  tableName: tag
  columns:
    id:
      type: integer
      primary: true
      autoincrement: true
    name:
      type:  string(255)

form widget:

class BlogPostForm extends BaseBlogPostForm {
    public function configure() {
        $autocompleteWidget = new sfWidgetFormChoice(array(
          'multiple'         => true,
          'choices'          => $this->getObject()->getTags(),
          'renderer_class'   => 'sfWidgetFormJQueryAutocompleterMany',
          'renderer_options' => array(
            'config' => '{
                json_url: "'.sfContext::getInstance()->getController()->genUrl('tag/autocomplete').'",
                json_cache: true,
                filter_hide: true,
                filter_selected: true,
                maxshownitems: 8
              }')
        ));
        $this->widgetSchema['tags_list'] = $autocompleteWidget;
    }
}

setup di un filtro custom

class CarFormFilter extends BaseCarFormFilter {
    public function 
configure() {
        
$this->setWidget('brand_id', new sfWidgetFormDoctrineChoice(array('model' => 'Brand''add_empty' => true)));
        
$this->setValidator('brand_id', new sfValidatorDoctrineChoice(array('required' => false'model' => 'Brand''column' => 'brand_id')) );
        
$this->validatorSchema->addOption('allow_extra_fields'true);
    }

    public function 
getFields() {
        
$a parent::getFields();
        
$a['brand_id'] = 'Number';
        return 
$a;
    }
    public function 
addBrandIDColumnQuery(Doctrine_Query $q$element$value) {
        
// r pare sia sempre l'alias della tabella principale, die(var_dump( $q )); per chiarirsi le idee
        $q
        
->leftJoin('r.Model m')
        ->
leftJoin('m.Brand b')
        ->
andWhere('m.brand_id = ?',  $value );
    }
    
// se occorre aggiungere più volte lo stesso alias, controllare che non sia già stato definito
    // uso: if( !$this->qry_contains($q, 'province p') ){$q->leftJoin('l.Province p');}
    function qry_contains($q$needle) {
        return (
strpos($q->getSql(), $needle)!==false);
    }
}

rsync

config/properties.ini
 
name=myproject
[production]
  host=myapp.example.com
  port=22
  user=myuser
  dir=/home/myaccount/myproject/

Don't confuse the production server (the host server, as defined in the properties.ini file of the project) with the production environment. Doing an rsync over SSH requires several commands, symfony automates this process with just one command:

# senza --go vengono mostrate le differenze esistenti
symfony sync production go

clear the cache in the production server after synchronization.

I18n Doctrine

schema.yml

News
:
  
tableNamenews
  columns
:
    
id:
      
typeinteger(8)
      
primarytrue
      autoincrement
true
    date
date(25)
    
urlstring(255)
    
tmbstring(255)
    
imagestring(255)
    
titlestring(255)
    
subtitlestring(255)
    
descriptionstring(255)
  
actAs:
    
I18n:
      
fields: [titlesubtitledescription]

#sf12 doctrine:build-all-reload

lib/form/BaseFormDoctrine.php

abstract class BaseFormDoctrine extends sfFormDoctrine
{
  public function 
setup()
  {
    if(
$this->isI18n()){
        
$this->embedI18n(array('en''it'));
        
$this->widgetSchema->setLabel('en''Inglese');
        
$this->widgetSchema->setLabel('it''Italiano');
    }
  }
}

// forza la lingua predefinita del backend  se da config non funziona
class myUser extends sfGuardSecurityUser /*sfBasicSecurityUser*/ {
  public function 
__construct(sfEventDispatcher $dispatchersfStorage $storage$options = array()) {
     
parent::__construct($dispatcher,$storage$options);
     
$this->setCulture('it_IT');
  }
}

//in routing.yml, abilita lo switch della lingua via /en/configurator
homepage:
  
url:   /:sf_culture/
  
param: { moduleconfiguratoractionindex }



//in apps/your_backend_application/config/settings.yml:
all:
  
settings:
    
i18n:    on
    default_culture
:     it

// in backend/i18n carica sf_admin.it.xml e sf cc

ORM

http://symforc.com/post/2008/07/10/Foreign-keys-onDelete-for-dummies

Doctrine

Delete Event

class Subscriber extends BaseSubscriber{
    public function 
preDelete($event) {
        
parent::preDelete($event);

        
$r Doctrine_Query::create()
          ->
select('s.*')
          ->
from('Subscription s')
          ->
where('s.subscriber_id = ?'$this->get('subscriber_id') )
          ->
execute();
          foreach( 
$r as $s){ $s->delete(); }
    }
}

richiedere un join esplicito delle tabelle

configurator.yml

config:
  list:
    table_method: retrieveBackendJobList
class SubscriberTable extends Doctrine_Table{
    public function 
retrieveBackendSubscriber(Doctrine_Query $q){
        
$rootAlias $q->getRootAlias();// ritorna 'r'
        $q->leftJoin($rootAlias.'.Locality l');
            
$q->leftJoin('l.Province p');
                
$q->leftJoin('p.State st');
        
$q->leftJoin($rootAlias.'.Subscription s');
            
$q->leftJoin('s.CopyApproximation co');
            
$q->leftJoin('s.Lang la');

        return 
$q;
    }
}

join implicito

doctrine genera automaticamente i left join necessari

$q Doctrine_Query::create()
            ->
from('Car c, c.Model m, m.Brand b, c.Fuel f, c.Gear g, c.Color p, c.Door d, c.CarImage, c.CarType')
            ->
where('c.car_id=?'$_GET['car_id'])
            -
limit(1);

SQL diretto

        $pdo = Doctrine_Manager::getInstance()->getCurrentConnection()->getDbh();
        $query = "SELECT max(0+number) as num FROM invoice WHERE anno = :anno";
        $stmt = $pdo->prepare($query);
        $stmt->execute( array(
          "anno"  => $year,
        ));
        $a = $stmt->fetch();
        return $a['num'];

Propel

debug

$sql = BasePeer::createSelectSql($c, $a=array() );

left Joins

$c = new Criteria();
            
$c->addJoinItemNotePeer::IDItemHasNotePeer::NOTE_IDCriteria::LEFT_JOIN);
            
$c->addJoinItemNotePeer::IDItemNoteI18nPeer::IDCriteria::LEFT_JOIN);
            
$c->add(ItemHasNotePeer::ITEM_ID$item_id);
            
$c->add(ItemNoteI18nPeer::CULTURE$this->getUser()->getCulture() );
            
$c->addSelectColumn(ItemNoteI18nPeer::TITLE);
            
$c->setPrimaryTableNameItemNotePeer::TABLE_NAME );
            
//var_dump( $c->toString() );
            $data ItemNotePeer::doSelectWithI18n($c);

configuration

app.yml configuration file contain any setting for your specific application. available through the global sfConfig class, and keys are prefixed with the app_ string:

sfConfig::get('app_active_days');

    
sfConfig::get('sf_environment') == 'prod'
    
sfConfig::get('sf_environment')== 'dev'

// altre chiavi interessanti
sfConfig::get('sf_lib_dir')
sfConfig::get('sf_root_dir')
sfConfig::get('sf_upload_dir')
sfConfig::get('sf_data_dir')
sfConfig::get('sf_symfony_lib_dir')
sfConfig::get('sf_apps_dir')
sfConfig::get('sf_debug')

Embedding Forms

// lib/form/doctrine/ProductForm.class.php
public function configure()
{
  
$subForm = new sfForm();
  for (
$i 0$i 2$i++)
  {
    
$productPhoto = new ProductPhoto();
    
$productPhoto->Product $this->getObject();

    
$form = new ProductPhotoForm($productPhoto);

    
$subForm->embedForm($i$form);
  }
  
$this->embedForm('newPhotos'$subForm);
}

Assets

Routing:

external:
  
url:   /external/:filename.:action
  param
: { moduleexternal}


Action:

class 
externalActions extends sfActions
{
  public function 
executeJs(sfWebRequest $request)
  {
    
sfConfig::set('sf_web_debug',false);
    
$this->setLayout(false);
    
$this->getResponse()->setHttpHeader('Content-type''text/javascript');
    
$this->setTemplate($this->getRequestParameter('action').'/'.$this->getRequestParameter('filename'));
    return 
'.'.$this->getRequestParameter('action');// ritorna il nome del template "$action.js.php"
  }
  public function 
executeCss(sfWebRequest $request)
  {
    
sfConfig::set('sf_web_debug',false);
    
$this->setLayout(false);
    
$this->getResponse()->setHttpHeader('Content-type''text/css');
    
$this->setTemplate($this->getRequestParameter('action').'/'.$this->getRequestParameter('filename'));
    return 
'.'.$this->getRequestParameter('action');// ritorna il nome del template "$action.js.php"
  }
}