Symfony World blog is not maintained anymore. Check new sys.exit() programming blog.

Custom query for admin generator

Scene from "Cabaret" by Bob Fosse (1972)

introduction

Do you want to use a specific query for retrieving objects used for running an admin generator? For example, you want to display information about related database records but you see that generating such page generates lots of SQL queries? It's very easy to set a custom query for admin generator in Symfony!


solution

You need to do only 1 simple thing - override the buildQuery method in your actions.class.php (of an admin module).

protected function buildQuery()
{
  return parent::buildQuery()
    ->leftJoin('p.Customer c');
}

parent::buildQuery() is the default query retrieving data for the admin. You may modify it as muh as you want (or you may even build your query from the beginning). The most important use of this feature is decreasing number of SQL queries, which makes an admin panel work a lot faster!

admin generator validation - validating integer input

An easy problem solving solution this time... If you want to have an integer validator for an input, the first thing you may think of using is:

$this->validatorSchema ['number'] =
  new sfValidatorInteger(array('required' => false));

and such code may cause you a lot of troubles because of the following error:
"%value%" is not an integer.
which may be frustrating to find the source of the problem. Integer validator handles validation on schema input value, which means, more or less, complex input (associative array). Need to use the sfValidatorSchemaFilter class:

$this->validatorSchema ['number'] =
  new sfValidatorSchemaFilter('text', new sfValidatorInteger(array('required' => false)));

And that's it!

Multiple database symfony configuration - part 2

introduction

In one of the previous posts I've briefly described how to set up multiple connections in a symfony project. This time I'll show how to set up multiple connections to distinct databases which have exactly the same structure.


example study case

Suppose we work for a company that runs few E-commerce shops that are based on the same open source software package (like magento, osCommerce or prestashop). Let's say, there are 3 different shops: shopA, shopB and shopC. Of course, each of them is provided with its own admin application (standard for open source software). And this creates a problem: the more different admin applications employees need to access, the more time it will take to do a simple task. The solution is to create a center administration application (written in symfony framework) to manage everything that is accessible in our shops (could be something else, not only shops). Therefore, our new symfony application will access 4 databases, 3 for shops: shop_a, shop_b, shop_c and the last, most important one, admin for the entire admin application which needs its own database.


and the problem is...

... model, forms and filter classes. The current version of Symfony framework forces us to create separate classes for each table in each database (symfony does NOT support accessing multiple databases with the same structure). For example, to provide access for our 3 shops for just one table, we need to generate lots of classes:

shop A shop B shop C
base model BaseShopAClass BaseShopBClass BaseShopCClass
model ShopAClass ShopBClass ShopCClass
model table ShopAClassTable ShopBClassTable ShopCClassTable
base form BaseShopAClassForm BaseShopBClassForm BaseShopCClassForm
form ShopAClassForm ShopBClassForm ShopCClassForm
base filter BaseShopAClassFormFilter BaseShopBClassFormFilter BaseShopCClassFormFilter
filter ShopAClassFormFilter ShopBClassFormFilter ShopCClassFormFilter
We don't need to have so many base classes, which define exactly the same model. So we'll do some tricks! They can be quite complicated at first sight, but don't worry.


useful tools

Something I probably wouldn't manage without is a versioning system (even if I'm the only developer). This is because lots of operations on files will be made - and a versioning system makes it really easy to work with. The following article is based on SVN.


the basic idea

We'll share access for a given table in all distinct databases within the same classes. Why? Here are some of the main reasons:

  1. Since all shop databases share the same structure (but different content - e.g. different pics, different prices), there's no need to create different (lib/form/filter) base classes for the ORM - they'd be the same afterall.
  2. By default, symfony libs generators will generate some useless junk, unfortunately. Most common situation is that all tables in databases shop_a, shop_b and shop_c will have the same names, like ps_product and ps_product_lang (representing products in prestashop). Symfony will create following files:
    • (model) BasePsProduct.class.php,
    • (model) PsProduct.class.php,
    • (model) PsProductTable.class.php,
    • (form) BasePsProductForm.class.php,
    • (form) PsProductForm.class.php,
    • (filter) BasePsProductFormFilter.class.php and
    • (filter) PsProductFormFilter.class.php.
    And the problem is described more or less here - PsProduct class (PsProductForm and PsProductFormFilter as well) will refer to THE LAST CONNECTION described in the databases.yml file. And there is, unfortunately, no effect if you put following code into your classes:
    Doctrine_Manager::getInstance()
      ->bindComponent('PsProduct', 'shop_a_connection');
  3. You don't have to rename tables in your shop databases. Usually open source packages use a table prefix: wp_ for wordpress, ps_ for prestashop and so on. If you want to create full set of classes for each database, you are forced to use a different prefix for each database (which may be what you don't want to do).
  4. Finally, the main reason in the long run: having full set of classes for each database, you are forced to copy all methods between ShopA, ShopB and ShopC model, form and filter classes. And this can easily lead to dozens of mistakes (copy-paste is supposed to be the source of 80% of all bugs). If only one base class is created, you won't have to copy-paste all methods and all relations in your project! If you stay with


schema files

Create separate schema files for admin and shop databases: all admin database structure goes to admin.yml file and the shared shop database structure goes to shop.yml file. Use default schema.yml file as a temporary/buffer file.


lib-reload script

Create batch directory inside your root project directory (the same which existed in symfony 1.0). Create a shell script in this directory called lib-reload.sh with the following code:

#!/bin/sh
 
echo 'deleting generated model/form/filter'
rm -fR lib/model
rm -fR lib/form
rm -fR lib/filter
echo 'model/form/filter deleted.'
 
echo 'restoring svn model.'
svn up lib/model
echo 'svn model restored.'
 
echo 'restoring svn form.'
svn up lib/form
echo 'svn form restored.'
 
echo 'restoring svn filter.'
svn up lib/filter
echo 'svn filter restored.'
The idea of this script is to:
  • generate all clas files (this includes useless junk, unfortunately),
  • commit changes you want to save,
  • delete ALL model, form and filter files,
  • then fetch all committed files from SVN
Now our libs include only those files which we need (all 'temporary' files are removed). You can find similar script in another article, outsourcing applications with symfony.


summary

I've described my solution for symfony access to many databases sharing the same structure (with different content). I'm absolutely aware of the fact that there may be better ways to solve this problem, but this one is the easiest & fastest - in my opinion. Please, feel free to share your opinions and suggestions!

Using tinyMCE with Symfony

Scene from "Dirty Dancing" by Emile Ardolino (1987)

Symfony gives you a possibility to use lots of third party libraries. One of them, extremely useful while developing backend of your application, is tinyMCE.

The symfony core team has created the sfFormExtraPlugin which helps you to attach tinyMCE to your projects. But there's a lot more you can do to make this editor satisfy your needs.

Configuring tinyMCE

  • I'm assuming you have properly installed the sfFormExtraPlugin inside your project.
  • First of all, you need to install tinyMCE on your own. Go to the download page, and put the extracted content inside your project's web directory. The downloaded archive includes tinyMCE usage examples, you may remove them and include only the tiny_mce directory inside your /web/js directory.
  • Right after including the tinyMCE files don't forget to attach them to the application: go to the backend/config/view.yml file and add the tiny_mce.js file. It should look similar to the following:
    javascripts:    [ /js/tiny_mce/tiny_mce.js ]
    TinyMCE is properly installed in our app already. Now it's time to set the inputs to use it.
  • Edit any of your lib/form classes and set the appropriate widget:
    class SubpageForm extends BaseSubpageForm
    {
      public function configure()
      {
        $this->widgetSchema['content'] =
          new sfWidgetFormTextareaTinyMCE();
      }
    }
    Run your backend application and check if everything is OK.

Advanced tinyMCE configuration

Lots of us may create websites for non-English communities, therefore custom language support is needed. Of course, there are more specific attributes you may want to change. Unfortunately, the sfWidgetFormTextareaTinyMCE class provided with the plugin is hardly configurable. The best solution is to create a custom class extending it and override all the attributes you need.

  • Create a sfWidgetFormTextareaTinyMCECustom class in the /lib/widget directory of your project:
    class sfWidgetFormTextareaTinyMCECustom extends sfWidgetFormTextareaTinyMCE
  • Override the render method. The main code that we are interested in is:
    $js = sprintf(<<<EOF
    <script type="text/javascript">
    tinyMCE.init({
        mode:                              "exact",
        elements:                          "%s",
        theme:                             "%s",
        plugins : "safari,spellchecker,pagebreak,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,inlinepopups,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template,wordcount,imagemanager,filemanager",
        language: "pl",
        %s
        %s
        theme_advanced_buttons1 : "save,newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect",
        theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor",
        theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen",
        theme_advanced_buttons4 : "insertlayer,moveforward,movebackward,absolute,|,styleprops,spellchecker,|,cite,abbr,acronym,del,ins,attribs,|,visualchars,nonbreaking,template,blockquote,pagebreak,|,insertfile,insertimage",
        theme_advanced_toolbar_location:   "top",
        theme_advanced_toolbar_align:      "left",
        theme_advanced_statusbar_location: "bottom",
        theme_advanced_resizing:           true
        %s
      });
     
    </script>
    EOF
  • Let's get our custom language working with the tinyMCE now. Go to the language pack download page. Choose the language(s) you want to install, check them (do not download the xml files since you'd have to compile them into the .js files) and press the download just below the end of the list. Move the zip archive into the tiny_mce directory and extract it there. All language files will be extracted in the appropriate places. So far, so good.
  • Edit the render method - add the following code inside the javascript code mentioned above:
    language: "pl",
    and your language pack should already work fine.
  • Since the default tinyMCE configuration doesn't include all possible features, you may add the following lines to enable all features:
    plugins : "safari,spellchecker,pagebreak,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,inlinepopups,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template,wordcount,imagemanager,filemanager",
    
    and
    theme_advanced_buttons1 : "save,newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect",
        theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor",
        theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen",
        theme_advanced_buttons4 : "insertlayer,moveforward,movebackward,absolute,|,styleprops,spellchecker,|,cite,abbr,acronym,del,ins,attribs,|,visualchars,nonbreaking,template,blockquote,pagebreak,|,insertfile,insertimage",
    
  • After those modifications your code should look like the following:
    $js = sprintf(<<<EOF
    <script type="text/javascript">
      tinyMCE.init({
        mode:                              "exact",
        elements:                          "%s",
        theme:                             "%s",
        plugins : "safari,spellchecker,pagebreak,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,inlinepopups,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template,wordcount,imagemanager,filemanager",
        language: "pl",
        %s
        %s
        theme_advanced_buttons1 : "save,newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect",
        theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor",
        theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen",
        theme_advanced_buttons4 : "insertlayer,moveforward,movebackward,absolute,|,styleprops,spellchecker,|,cite,abbr,acronym,del,ins,attribs,|,visualchars,nonbreaking,template,blockquote,pagebreak,|,insertfile,insertimage",
        theme_advanced_toolbar_location:   "top",
        theme_advanced_toolbar_align:      "left",
        theme_advanced_statusbar_location: "bottom",
        theme_advanced_resizing:           true
        %s
      });
    </script>
    EOF
    
    Of course, you may not need all of the tinyMCE features, feel free to modify the code.
  • Just to make sure, such installation enables all tinyMCE features on your frontend application, such as emoticon images. Play with it!

Hope everything will work fine. Any feedback is appreciated.

Symfony newsletter using Swift mailer

Scene from "Postman Pat" by John Cunliffe & Ivor Wood

introduction

I had a task to implement a newsletter system for a sf 1.4 project. It was quite simple: an anonymous user from outside can subscribe to the newsletter (storing his E-mail address in the database). The site admin can create a newsletter article (with simple properties like title and content). When the send button is pressed, this article is sent to all subscribed E-mails.

output escaping

The newsletter article content value is modified using javascript rich editor such as tinyMCE or FCK Editor, so it has HTML tags like <p>. I forced a problem that either Symfony or Swift was escaping all those HTML tags. I was trying to find solution using methods like getRawValue, getRaw or anything like that, but it turned out that it was Swift Mailer who was the cause of the problem. Following this small tutorial, I needed to add only one line of code:

$message->setContentType("text/html");
Now all formatting was shown properly.

final code solution

I hope that some of you may find this piece of code useful:

public function executeListSend()
{
  $newsletter = $this->getRoute()->getObject();
  if ($newsletter->getSent())
  {
    $this->getUser()->setFlash('notice',
      'The selected newsletter has already been sent.');
  }
  else
  {
    $address_collection = Doctrine::getTable('Address')
      ->getAllAddressesQuery()
      ->fetchArray();
 
    $addresses = array();
    foreach($address_collection as $address)
      $addresses[] = $address['address'];
 
    $message = $this->getMailer()->compose(
      array('name.surname@gmail.com' => 'sitename'),
      $addresses,
      $newsletter->getTitle(),
      $newsletter->getContent()
    );
    $message->setContentType("text/html");
    $this->getMailer()->send($message);
 
    $newsletter->setSent(true);
    $newsletter->save();
 
    $this->getUser()->setFlash('notice',
      'The selected newsletter has been sent successfully.');
  }
  $this->redirect('@newsletter');
}
Implementing a newsletter with Symfony & Swift is really easy!