Tuesday, August 27, 2013

The need to state EntityManager name in a UniqueEntity validation constraint [Symfony2, Doctrine2]

I need to code a backoffice that manages many entities of my sites. Each of my sites has its own bundle with its own database, and a dependency to a common bundle with its own "common" database (for users, logging, billing).

For the same reasons, avoid to access default EntityManager from Container, without naming it... It could lead to an error if you embed your bundle in another application that states another EntityManager as default one...

Doctrine config for site#1

# Doctrine Configuration
doctrine:
    dbal:
        default_connection: site1
        connections:
            site1:
                ...
            common:
                ...
    orm:
        auto_generate_proxy_classes: %kernel.debug%

        default_entity_manager: site1
        entity_managers:
            site1:
                connection: site1
                ...
            common:
                connection: common
                ...

Doctrine config for site#2

# Doctrine Configuration
doctrine:
    dbal:
        default_connection: site2
        connections:
            site2:
                ...
            common:
                ...
    orm:
        auto_generate_proxy_classes: %kernel.debug%

        default_entity_manager: site2
        entity_managers:
            site2:
                connection: site2
                ...
            common:
                connection: common
                ...

Doctrine config for backoffice

# Doctrine Configuration
doctrine:
    dbal:
        default_connection: common
        connections:
            common:
                ...
            site12:
                ...
            site2:
                ...
    orm:
        auto_generate_proxy_classes: %kernel.debug%

        default_entity_manager: common
        entity_managers:
            common:
                connection: common
                ...
            site1:
                connection: site1
                ...
            site2:
                connection: site2
                ...

If in site#1 I handle this entity with UniqueEnity constraint, I need to state entityManager name if I don't want to get an error like "Call to a member function getClassMetadata() on a non-object" (Doctrine does not throw Exception Registry::getManagerForClass($class) gives no result).


namespace AlterPHP\Site1Bundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * AlterPHP\Site1Bundle\Entity\EntityForSite1
 * @ORM\Entity() *
 * @UniqueEntity(fields={"slug"}, em="site1")
*/
class EntityForSite1
{
    ...
}

Thursday, November 29, 2012

Symfony on non-standard ports

As Symfony (2.0) doc is not very clear on this subject, I spent some time to help a colleague on the following question : How to make HTTPS port different from 443 ?

Of course it's possible, and this is a simple configuration option in app/config.yml :

framework:

   ...

    # router configuration
    router:
        ...
        http_port:            81
        https_port:           1443

    ...

Official doc shows it in the Reference chapter, but not in table of indexes... Let's see http://symfony.com/doc/2.0/reference/configuration/framework.html#full-default-configuration

Tuesday, October 16, 2012

How to inhibit Soap Security headers "mustUnderstand" attribute in a Symfony2 controller

I never managed to make native PHP SoapServer class to handle "understand" soap:Security header as expected with mustUnderstand attribute. But some WS tools like SoapUI automatically set it to "true" or "1" and I needed to make a work around to avoid Soap Fault : "SOAP-ENV:MustUnderstand / Header not understood"

Here is a controller action that inhib mustUnderstand attribute :

   public function ordersFollowUpAction()
   {
      $request = $this->getRequest();

      //This deletes mustUnderstand attribute from raw request content
      $decodedRequest = preg_replace('/ ([-\w]+\:)?(mustUnderstand=")(1|true)(")/', '', $request->getContent());

      $server = new \SoapServer(__DIR__ . '/../Resources/public/wsdl/MyWsdl.wsdl');

      $server->setObject($this->get('my_webservice'));

      $response = new Response();
      $response->headers->set('Content-Type', 'text/xml; charset=UTF-8');

      try
      {
         ob_start();
         //This explicitely passes clear request content to the service handler
         $server->handle($decodedRequest);
         $response->setContent(@ob_get_clean());
      }
      catch (\Exception $e)
      {
         $this->get('logger')->err($e->getMessage());
         $response->setContent('An error occured, please contact our support service.');
      }

      return $response;
   }

How to read Soap WSSE headers

While following Symfony2 WSSE implementation tutorial, I was annoyed by the suggested WSSE headers interpretation. It only presents HTTP headers, not Soap WSSE headers...

Here is an extract of Symfony2 tutorial, handling HTTP headers :

        $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
        if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) {
            return;
        }

Obviously, it doesn't fit Soap headers that look like that :


  
   2012-10-10T12:00:36.099Z
   2012-10-10T12:05:36.099Z
  
  
   username@email.com
   E2wz/+mTabxWO37Hk6UeqSJF8+o=
   sAg8czv5GmW7rUKgqhm1Qw==
   2012-10-10T12:00:36.099Z
  
 

To read these headers, I use SimpleXML after having formatted the raw request content :

<?php

namespace AlterPHP\SoapBundle\Tools;

use Symfony\Component\HttpFoundation\Request;

/**
 * Description of WsseHeadersDecoder
 *
 * @author pece
 */
class WsseHeadersDecoder
{

   public function getHeaders(Request $request)
   {
      //HTTP headers (as described here : http://symfony.com/doc/2.0/cookbook/security/custom_authentication_provider.html#the-listener
      if ($request->headers->has('x-wsse'))
      {
         $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
         if (1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches))
         {
            return false;
         }
         else
         {
            $username = $matches[1];
            $passwordDigest = $matches[2];
            $nonce = $matches[3];
            $created = $matches[4];
         }
      }
      //Classic SOAP headers
      else
      {
         //Clear XML namespace prefixes to handle with SimpleXML
         $decodedRequest = preg_replace("/(<\/?)([-\w]+):([^>]*>)/", "$1$3", $request->getContent());
         $xmlRequest = simplexml_load_string($decodedRequest);

         if (
            !isset($xmlRequest->Header->Security->UsernameToken->Username)
            || !isset($xmlRequest->Header->Security->UsernameToken->Password)
            || !isset($xmlRequest->Header->Security->UsernameToken->Nonce)
            || !isset($xmlRequest->Header->Security->UsernameToken->Created)
         )
         {
            return false;
         }
         else
         {
            $username = (string) $xmlRequest->Header->Security->UsernameToken->Username;
            $passwordDigest = (string) $xmlRequest->Header->Security->UsernameToken->Password;
            $nonce = (string) $xmlRequest->Header->Security->UsernameToken->Nonce;
            $created = (string) $xmlRequest->Header->Security->UsernameToken->Created;
         }
      }

      return array (
              'username' => $username, 'passwordDigest' => $passwordDigest, 'nonce' => $nonce, 'created' => $created
      );
   }

}

Monday, August 6, 2012

How to deal with asynchronous request with Symfony2 and other PHP frameworks

As well as most of PHP frameworks, Symfony2 manages user session by locking it during request treatment. It avoids concurrent access to session file but makes requests to be queued until session lock is freed...

This behaviour is really well explained in this post from Gareth McCumskey.

As Gareth describes his solution, we need to manually free session lock after all session modifications happen in the request treatment. Here is a pattern for controllers that handle AJAX calls (solution provided by Matt on stackoverflow) :

<php

// ...

myControllerAction()
{
    //Actions that modify session
    // ...
 
    //We free session lock as no more session modifications will happen in next actions !
    $session = $this->get('session');
    $session->save();
    session_write_close();
 
    //Actions that don't modify session
    // ...
 
    $this->return('...');
}

Here is the result for an action that process a 10 seconds sleep() :
Before :
After :