Thursday, September 5, 2013

Symfony2 Behat and Emails

I've been using Behat with my Symfony apps for some time now but recently I needed to include emails in my features. I found tutorials at http://docs.behat.org/cookbook/using_the_profiler_with_minkbundle.html and http://docs.behat.org/cookbook/intercepting_the_redirections.html but they did not work out of the box for me. I guess they are now slightly out of date. After some time searching the docs and source code. I found that I needed to enable the profiler in the test environment:

# app/config/config_test.yml
framework:
    profiler:
        enabled: true

I tried leaving this set to false and enabling the the profiler for the next request with a step but it always said the profiler was diabled

# src/.../...Bundle/Features/Context/FeatureContext.php
    /**
     * @When /^(?:|I )enable the profiler$/
     */
    public function iEnableProfiler()
    {
        $driver = $this->getSession()->getDriver();
        $driver->getClient()->enableProfiler();
    }

Also I needed to change the instance of checks from GoutteDriver and SymfonyDriver to KernelDriver. Also I did not need to tag the scenario @mink:symfony as the default session in behat.yml is set to symfony2. Below is what I ended up with. Hope this helps someone else.

# src/.../...Bundle/Features/Context/FeatureContext.php
    public function getSymfonyProfile()
    {
        $driver = $this->getSession()->getDriver();
        if (!$driver instanceof KernelDriver) {
            throw new UnsupportedDriverActionException(
                'You need to tag the scenario with '.
                '"@mink:symfony". Using the profiler is not '.
                'supported by %s', $driver
            );
        }

        $profile = $driver->getClient()->getProfile();
        if (false === $profile) {
            throw new \RuntimeException(
                'Emails cannot be tested as the profiler is '.
                'disabled.'
            );
        }

        return $profile;
    }

    public function canIntercept()
    {
        $driver = $this->getSession()->getDriver();
        if (!$driver instanceof KernelDriver) {
            throw new UnsupportedDriverActionException(
                'You need to tag the scenario with '.
                '"@mink:goutte" or "@mink:symfony". '.
                'Intercepting the redirections is not '.
                'supported by %s', $driver
            );
        }
    }


# src/.../...Bundle/Features/....feature
  Scenario: Send an email
    Given I am on the homepage
    When I fill in ...
    And I press "Send message" without redirection
    Then I should get an email to "..." with "..."
    And I should be redirected

Wednesday, March 28, 2012

Geocoding in Symfony2 and Doctrine2


I needed to add latitude and longitude fields in an entity so I could plot it to a map and figure distances etc. This is a pretty common thing but I wanted to make it as simple and elegant as possible using Symfony2 and Doctrine2.
To start with I selected a geocoding library instead of trying to roll my own, remember we are keeping this simple. I choose Geocoder which has great features and is dead simple to use. First we need to install the Geocoder and an http adapter for it (I used Buzz a great project on its own) into our Symfony2 project.
# deps
[geocoder]
    git=http://github.com/willdurand/Geocoder.git

[buzz]
    git=http://github.com/kriswallsmith/Buzz.git
# app/autoload.php
$loader->registerNamespaces(array(
    //....
    'Geocoder'         => __DIR__.'/../vendor/geocoder/src',
    'Buzz'             => __DIR__.'/../vendor/buzz/lib',
));
and then run
php bin/vendors install
That's it for install, easy right? Now to use it. You could just follow the docs and create an instance whenever you needed to use but this would require you configure it each time; not what I call elegant. Symfony2's Service Container or DIC (dependency injection container) to the rescue.
# app/config/config.yml
services:
  geocoder.adapter:
    class:  Geocoder\HttpAdapter\BuzzHttpAdapter
  geocoder.address:
    class:  Geocoder\Provider\GoogleMapsProvider
    arguments: [@geocoder.adapter]
  geocoder.ip:
    class:  Geocoder\Provider\FreeGeoIpProvider
    arguments: [@geocoder.adapter]
Symfony2's Service Container is a great feature you can find out more here. Now we can use this service from a controller action or anywhere with the service container.
$geocoder = $this->get('geocoder.address');
var_dump($geocoder->getGeocodedData('41 East 4th Street Cookeville, TN 38501'));

var_dump($this->get('geocoder.ip')->getGeocodedData('8.8.8.8'));
Now with only a couple of minutes of work, we have it working and able to geocode physical addresses or ip addresses. Now I could edit the entity's controller action and set the latitude and longitude whenever it was saved but I wanted something more reusable and elegant. Enter Doctrine2 events.
# src/MS/RentrBundle/Doctrine/Event/GeocoderEventSubscriber.php
namespace MS\RentrBundle\Doctrine\Event;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Geocoder\Provider\ProviderInterface;

/**
 * Subscribes to Doctrine prePersist and preUpdate to update an Apartment's latitude and longitude
 *
 * @author msmith
 */
class GeocoderEventSubscriber implements EventSubscriber {
    protected $geocoder;
    
    public function __construct(ProviderInterface $geocoder){
        $this->geocoder = $geocoder;
    }

    /**
    * Specifies the list of events to listen
    *
    * @return array
    */
    public function getSubscribedEvents(){
        return array(
            'prePersist',
            'preUpdate',
        );
    }
    
    /**
     * Sets a new Apartment's latitude and longitude if not present 
     * 
     * @param LifecycleEventArgs $eventArgs 
     */
    public function prePersist(LifecycleEventArgs $eventArgs){
        if(($apartment = $eventArgs->getEntity()) instanceof \MS\RentrBundle\Entity\Apartment){
            if( !$apartment->latitude || !$apartment->longitude){
                $this->geocodeApartment($apartment);
            }
        }
    }
    
    /**
     * Sets an updating Apartment's latitude and longitude if not present 
     * or any part of address updated
     * 
     * @param PreUpdateEventArgs $eventArgs 
     */
    public function preUpdate(PreUpdateEventArgs $eventArgs){
        if(($apartment = $eventArgs->getEntity()) instanceof \MS\RentrBundle\Entity\Apartment){
            if( !$apartment->latitude || !$apartment->longitude 
                || $eventArgs->hasChangedField('street') || $eventArgs->hasChangedField('city') 
                || $eventArgs->hasChangedField('state') || $eventArgs->hasChangedField('zip')){
                $this->geocodeApartment($apartment);
                
                $em = $eventArgs->getEntityManager();
                $uow = $em->getUnitOfWork();
                $meta = $em->getClassMetadata(get_class($apartment));
                $uow->recomputeSingleEntityChangeSet($meta, $apartment);
            }
        }
    }
    
    /**
     * Geocode and set the Apartment's latitude and longitude
     * 
     * @param type $apartment 
     */
    private function geocodeApartment($apartment){
        $result = $this->geocoder->getGeocodedData($apartment->getAddress());
        $apartment->latitude = $result['latitude'];
        $apartment->longitude = $result['longitude'];
    }
    
}
# app/config/config.yml
services:
  # ...

  geocoder.listener:
    class:  MS\RentrBundle\Doctrine\Listener\GeocoderEventSubscriber
    arguments: [@geocoder.address]
    tags:
      - { name: doctrine.event_subscriber }
The event subscriber is a little more verbose then I would like and you could abstract out the entity to make it reusable for multiple entity types but you would need to allow for configuration which could be accomplished with the service container. The main things to notice it that the service container passes in our configured geocoder instance, prePersist() handles the insert and preUpdate() handles all updates to the entity. To get our new subscriber working with Doctrine2 all we need to do is give it the tag "doctrine.event_subscriber". That's it now we have a full functioning geocoding solution that is simple and elegant.