Monday, December 5, 2011

How to create a Service to embed business logic functions in Symfony2

Services are classes where you will write the Business Logic code or any other treatment that you want to make accessible to a controller or another service. The Logger delivered with Symfony2 is a service, like the Registry of Doctrine, which gives access to the EntityManager.

The example below shows how to create a service, to pass two services it can use as parameters.
1- We begin by defining our service in the configuration file for services of our bundle (AcmeDemoBundle) :
src/Acme/DemoBundle/Resources/config/services.xml
<?xml version="1.0" ?>
 

 
    
        
          
        
 
        
          
        
 
        
        
            
            
            
        
        
    
 

Was added a node , passed the service name in the id attribute and PHP class in class attribute.
As parameters we added 2 of type "service" : the Doctrine registry service (id = "doctrine") and logger (id = "logger").
We added one more to which we return later ...

Monday, October 31, 2011

Set up an AuthenticationSuccessHandler in Symfony2

Symfony2 allows you to simply make treatments after a login attempt success. As simple as you just have to define a service, called "security.authentication.success_handler" and make it to implement AuthenticationSuccessHandlerInterface...

In this example, I want logged new user to be redirected to a "Terms and conditions" accept form. I have to check the boolean property termsAndConditionsOk of the User entity to know if redirection applies.
AuthenticationSuccessHandlerInterface defines 1 method :
<?php

/*
 * This file is part of the Symfony framework.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace Symfony\Component\Security\Http\Authentication;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Interface for a custom authentication success handler
 *
 * If you want to customize the success handling process, instead of
 * overwriting the respective listener globally, you can set a custom success
 * handler which implements this interface.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface AuthenticationSuccessHandlerInterface
{
    /**
     * This is called when an interactive authentication attempt succeeds. This
     * is called by authentication listeners inheriting from
     * AbstractAuthenticationListener.
     *
     * @param Request        $request
     * @param TokenInterface $token
     *
     * @return Response the response to return
     */
    function onAuthenticationSuccess(Request $request, TokenInterface $token);
}

Definition of the service in a services.yml config file (i use YAML) :

Tuesday, October 4, 2011

Deal with a nullable relation field in an Entity with Doctrine2

Entity generation from an existing database is a long way to go... Last issue : set a relation field to NULL => the generated setter expects an Entity Object as a parameter

MyEntity is an entity including a field myRelatedEntity referencing an entity MyRelatedEntity. Basic generation produces this setter :
class MyEntity
{

    ...

    /**
     * Set myRelatedEntity
     *
     * @param Xxx\YourBundle\Entity\MyRelatedEntity $myRelatedEntity
     */
    public function setMyRelatedEntity(MyRelatedEntity $myRelatedEntity)
    {
        $this->myRelatedEntity = $myRelatedEntity;
    }

    ...

}
This setter doesn't allow to set NULL value. Instead you get this kinf of error :
Catchable fatal error: Argument 1 passed to setMyRelatedEntity() must be an instance of \Xxx\YourBundle\Entity\MyRelatedEntity, null given
This indicates you have to modify the setter to accept null value :
class MyEntity
{

    ...

    /**
     * Set myRelatedEntity
     *
     * @param mixed $myRelatedEntity Entity MyRelatedEntity or NULL
     */
    public function setMyRelatedEntity(MyRelatedEntity $myRelatedEntity = null)
    {
        $this->myRelatedEntity = $myRelatedEntity;
    }

    ...

}

Configuration trick for multiple entity managers in Symfony2

First time I tried to define multiple entity managers in config.yml, i got some errors...


I didn't strictly followed symfony cookbook but based my configuration on the reference about Doctrine configuration. So I let "auto_mapping" parameter with false value, and I got following error :

InvalidConfigurationException: Unrecognized options "auto_mapping" under "doctrine.orm"

So if you want to implement many entity managers, don't forget to erase "auto_mapping" param in your config files (config.yml and derived ones if needed).
doctrine: 
    orm: 
        auto_mapping: false 
        default_entity_manager:   em1 
        entity_managers: 
            em1: 
                connection:       conn1 
                mappings: 
                    XxxYourBundle: ~ 
            em2: 
                connection:       conn2 
                mappings: 
                    XxxYourBundle2: ~

Monday, September 26, 2011

Deal with SET field in your entities with Doctrine2 / Symfony2

Given MyEntity is an entity (mapping table my_entity), given mySetField is a MySQL SET field from this entity (mapping column my_set_field). Its admissible values are 'Value1', 'Value2' and 'Value3' :
<?php

namespace MyCompany\MyBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * MyCompany\MyBundle\Entity\MyEntity
 */
class MyEntity
{
   //SET de la colonne my_set_field
   const MY_SET_FIELD_VALUE1 = 'Value1';
   const MY_SET_FIELD_VALUE2 = 'Value2';
   const MY_SET_FIELD_VALUE3 = 'Value3';

   static private $_mySetFieldValues = null;

   /**
    * @var array $mySetField
    */
   private $mySetField;

   static public function getMySetFieldChoices()
   {
      // Build $_mySetFieldValues only if this is the first call
      if (self::$_mySetFieldValues == null)
      {
         self::$_mySetFieldValues = array ();
         $oClass = new \ReflectionClass('\MyCompany\MyBundle\Entity\MyEntity');
         $classConstants = $oClass->getConstants();
         $constantPrefix = "MY_SET_FIELD_";
         foreach ($classConstants as $key => $val)
         {
            if (substr($key, 0, strlen($constantPrefix)) === $constantPrefix)
            {
               self::$_mySetFieldValues[$val] = $val;
            }
         }
      }
      return self::$_mySetFieldValues;
   }

   /**
    * Set mySetField
    *
    * @param array $mySetField
    */
   public function setMySetField(array $mySetField)
   {
      foreach ($mySetField as $mySetValue)
      {
         if (!in_array($mySetValue, self::getMySetFieldChoices()))
         {
            throw new \InvalidArgumentException(
               sprintf('Invalid value for my_entity.my_set_field : %s.', $mySetValue)
            );
         }
      }

      $this->mySetField = implode(',', $mySetField);
   }

   /**
    * Get mySetField
    *
    * @return array
    */
   public function getMySetField()
   {
      return explode(',', $this->mySetField);
   }

}
We will no longer refer to values ​​of the field but to the constants, it allows to centralize these values ​​somewhere ! PS: Thanks to guyaloni (http://forum.symfony-project.org/viewtopic.php?f=23&t=37406&p=125387#p125382) for the ReflectionClass enhancement ;)

Sunday, September 18, 2011

Deal with ENUM fields in your entities with Doctrine2 / Symfony2

MyEntity is an entity (mapping table my_entity), myEnumField is a MySQL ENUM field from this entity (mapping column my_enum_field). Its admissible values are 'Value1', 'Value2' and 'Value3' :
<?php

namespace MyCompany\MyBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * MyCompany\MyBundle\Entity\MyEntity 
 */
class MyEntity
{
   //ENUM de la colonne my_enum_field 
   const MY_ENUM_FIELD_VALUE1 = 'Value1';
   const MY_ENUM_FIELD_VALUE2 = 'Value2';
   const MY_ENUM_FIELD_VALUE3 = 'Value3';

   static private $_myEnumFieldValues = null;

   /**
    * @var string $myEnumField 
    */
   private $myEnumField;

   static public function getMyEnumFieldChoices()
   {
      // Build $_myEnumFieldValues if this is the first call
      if (self::$_myEnumFieldValues == null)
      {
         self::$_myEnumFieldValues = array ();
         $oClass = new \ReflectionClass('\MyCompany\MyBundle\Entity\MyEntity');
         $classConstants = $oClass->getConstants();
         $constantPrefix = "MY_ENUM_FIELD_";
         foreach ($classConstants as $key => $val)
         {
            if (substr($key, 0, strlen($constantPrefix)) === $constantPrefix)
            {
               self::$_myEnumFieldValues[$val] = $val;
            }
         }
      }
      return self::$_myEnumFieldValues;
   }

   /**
    * Set myEnumField 
    * 
    * @param string $myEnumField 
    */
   public function setMyEnumField($myEnumField)
   {
      if (!in_array($myEnumField, self::getMyEnumFieldChoices()))
      {
         throw new \InvalidArgumentException(
            sprintf('Invalid value for my_entity.my_enum_field : %s.', $myEnumField)
         );
      }

      $this->myEnumField = $myEnumField;
   }

}
We will no longer refer to values ​​of the field but to the constants, it allows to centralize these values ​​somewhere ! PS: Thanks to guyaloni (http://forum.symfony-project.org/viewtopic.php?f=23&t=37406&p=125387#p125382) for the ReflectrionClass enhancement ;)

Deal with MySQL types natively unsupported by Doctrine2

Some types of data, specific to MySQL are not supported by Doctrine2 when generating the mapping and entities from an existing database (reverse engineering).

Nevertheless, there remains the possibility to easily convert them into basic types. Example with ENUM, VARBINARY and TINYBLOB types.

Everything goes in app/config.yml, in the "doctrine" section (only conf, very convenient), see the "mapping_types" attribute :
# Doctrine Configuration doctrine: dbal: default_connection: default connections: default: driver: %database_driver% host: %database_host% port: %database_port% dbname: %database_name% user: %database_user% password: %database_password% charset: UTF8 mapping_types: enum: string varbinary: string tinyblob: text connection1: ... orm: auto_generate_proxy_classes: %kernel.debug% auto_mapping: true

Monday, August 8, 2011

Use ApcCache to cache any PHP data in Symfony2

APC is the most well-known caching system for PHP, and maybe one of the simpliest. Symfony2 strongly suggests to enable it, as well on development than production environment.


Sf2 natively uses APC to cache PHP files as OPCODE. But you can cache what you want in your code, any kind of PHP data.

APC makes the distinction between PHP OPCODE cache and custom cache :
  • File cache gets PHP OPCODE,
  • User cache gets what developer decides to cache.

Here is an example of a controller action displaying some RSS streams on a homepage. We don't want to load the stream each time a user browses homepage so we put in cache the whole response (!) :

   /**
    * Route ; _home_rssNews
    * @return Symfony\Component\HttpFoundation\Response
    */
   public function rssAction()
   {
      //We get the cache before anything else
      $cacheDriver = new ApcCache();

      //If the cache exists and is not expired for _home_rssNews, we simply return its content !

      if ($cacheDriver->contains('_home_rssNews'))
      {
         return $cacheDriver->fetch('_home_rssNews');
      }

      //If not, we build the Response as usual and then put it in cache !

      $viewVars = array ();

      // FLUX RSS BLOG
      $rssB = $this->fluxRssBlog('my_blog_rss_stream'));
      //FLUX RSS TWITTER
      $rssT = $this->fluxRssTwitter('my_twitter_rss_stream'));

      $viewVars['rssB'] = $rssB;
      $viewVars['rssT'] = $rssT;
      $response = $this->render('XxxYourBundle:Default:rssNews.html.twig', $viewVars);

      //We put this response in cache for a 15 minutes period !
      $cacheDriver->save('_home_rssNews', $response, "900");

      return $response;
   }

Thursday, August 4, 2011

Redirection with Cookie sending in Symfony2

As i apply a redirection after switching language, i'd like to send a cookie to store the last selected language for the user. But Symfony2 RedirectResponse default constructor doesn't allow to do this....

Here is a class extanding RedirectResponse that takes an array of Cookie objects as third parameters :

This component is downloadable and forkable on my Git repository : http://github.com/alterphp/components

<?php

/*
 * A RedirectResponse object with cookie sending
 */

namespace AlterPHP\Component\HttpFoundation;

use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * RedirectResponseWithCookie represents an HTTP response doing a redirect and sending cookies.
 */
class RedirectResponseWithCookie extends RedirectResponse
{

   /**
    * Creates a redirect response so that it conforms to the rules defined for a redirect status code.
    *
    * @param string  $url    The URL to redirect to
    * @param integer $status The status code (302 by default)
    * @param Symfony\Component\HttpFoundation\Cookie[] $cookies An array of Cookie objects
    */
   public function __construct($url, $status = 302, $cookies = array ())
   {
      parent::__construct($url, $status);

      foreach ($cookies as $cookie)
      {
         if (!$cookie instanceof Cookie)
         {
            throw new \InvalidArgumentException(sprintf('Third parameter is not a valid Cookie object.'));
         }
         $this->headers->setCookie($cookie);
      }
   }
}

Example :

/* in a controller action */

$cookie = new Cookie('Language', $culture, '2037-01-01');

return new RedirectResponseWithCookie($this->generateUrl($route, $params), 302, array ($cookie));

Wednesday, August 3, 2011

Disable client-side HTML5 form validation while developing

HTML5 provides new features that will make the job simplier for web developers, and also for users. Among them, the client-side validation with required attribute.


Symfony2 framework enables the generation of this attribute in its forms. But when i'm developping, i don't want this client-side validation cause it's locking the process before the server-side validation. I want my site to be compatible with HTML4 and HTML5 so i can't rely on this client-side validation (anyway i never rely on client-side treatments) and i don't want to use a HTML4 browser for development. So I looked for an option in FF5 to disable this browser HTML5 feature, but i never found it...

So, I used JavaScript and jQuery and wrote this piece of code :

window.onload = function ()
{
  $("[required]").each(function()
    {
      $(this).removeAttr('required');
    }
  );
}

Then winzou suggested a better solution with less form tags than input to treat :

window.onload = function ()
{
  $('form').each(function()
    {
      $(this).attr('novalidate', 'novalidate');;
    }
  );
}

And i include this script in my Twig layout, only in dev environment :

{% if app.environment == 'dev' %}
<script src="{{ asset('bundles/mybundle/js/dev.js') }}" type="text/javascript"></script>
{% endif %}

Tuesday, June 14, 2011

Some tips for Doctrine2 entities' generation from an existing database

So I began working on Symfony2 framework, i had to map an existing database with Doctrine2. First of all, keep in mind  that Doctrine2 gives its best for creating a schema, not for generating an object model from an existing one.

It's therefore important to "prepare" your database before generation unless you'll get an approximative model...


  • You must define a Primary Key on each table of your DB. It seems obvious but Doctrine2 will fail otherwise...

  •  Composite primary keys including only foreign keys is not yet fully supported by Doctrine2 (see documentation). Think about adding an auto-increment integer as PK and apply a UNIQUE index on foreign keys from old composite PK.

  • Natively, Doctrine2 doesn't natively resolve BIT, BINARY, VARBINARY, TINYBLOB, MEDIUMBLOB, BLOB, LONGBLOB, ENUM, SET, GEOMETRY, POINT, MULTIPOINT, LINESTRING, MULTILINESTRING, POLYGON and MULTIPOLYGON.

  • Important : Doctrine2 converts SQL type TINYINT to PHP type boolean. Use at least a SMALLINT if you want to mean an integer.

  • Doctrine automatically assignes IDs with generator (PHP equivalent to auto_increment) but it does it for all types of simple Primary Keys, even strings and foreign keys...

When you're satisfied of your schema, you can generate the metadatas with the command CLI interface :

Thursday, May 26, 2011

Error display on FileType form field with Symfony2

By default, error_bubbling is false on all form fields in Symfony2. All but FileType (and some others ?) !


So if you don't want error_bubbling, don't forget to explicitely specify if as following :

$builder->add('myFileField', 'file', array ('label' => 'myform.upload.label', 'error_bubbling' => false));