Including Foreign Keys in Doctrine2 Array Results

Recently I was working on a project where part of it was doing data exports. Exports on the surface are quick and easy - query the database, put it into the export format, send it over to the user. However, as a data set grows, exports become more complicated. Now processing it in real time no longer works as it takes too long or too much memory to export. This is why I’ll almost always use a background process (notified via Gearman) to process the data and notify the user when the export is ready for download. On separate background threads you can have different memory limits and not worry about a request timeout. I suggest trying to not use Doctrine’s objects for the export, but get the query back in array format (via getArrayResult). Doctrine objects are great to work with, but expensive in terms of time to populate and memory usage; if you don’t need the object graph results in array format are much quicker and smaller memory wise.

On this specific export I was exporting an entity which had a foreign key to another table that needed to be in the export. I didn’t want to create a join over the entire data set as it was unnecessary. For example, a project which has a created by user as a relation. If I simply did the following:

// In ProjectRepository
public function getBaseExportQuery()
{
  return $this->createQueryBuilder('p')
              ->getQuery()
              ->getArrayResult();
  
}

I’d end up with an array which had all the project columns except any that are defined as a foreign key. This means in my export I couldn’t output the “Created by user id” as it wasn’t included in the array. It turns out that Doctrine already has this exact situation accounted for. To include the FK columns you need to set a hint on the query to include meta columns to true. The updated query code would look similar to:

// In ProjectRepository
public function getBaseExportQuery()
{
  return $this->createQueryBuilder('p')
              ->getQuery()
              ->setHint(\Doctrine\ORM\Query::HINT_INCLUDE_META_COLUMNS, true)
              ->getArrayResult();
  
}

Now you can include the foreign key columns without doing an joins on a query that returns an array result set.

HotelSaver.io - Risk Free Price Monitoring For Existing Hotel Reservations

Last week we officially did a very quiet launch of HotelSaver.io. The concept is fairly simple: You submit your existing hotel reservation, we constantly monitor for price drops, if we find one we notify you immediately and you save money. I had the idea for this site a few months ago when I had made reservations in New Orleans for a bachelor party, then noticed the next day the prices dropped and I managed to save over 40% of the reservation by getting it price matched and discounted. At this point I thought “wow, that was incredibly easy, took little time and saved a ton of money”. Shortly thereafter and shooting it around with everyone here, it was decide we’re going to launch a MVP product and see what the reception is.

With this concept it is really easy to quickly blow it up into a massive product with complex algorithms, payment types, etc. however trying to follow our own advice to our clients we launched with the minimal features to make it useful to the end user: simple reservation monitoring and payment processing to get us paid. We knew the first version of the product would be far from finished in terms of design and feature complete, but we wanted to see if others thought the idea had legs. Here are a few things we did to cut down on the time to launch even more:

  • The initial design is based on a free template we found which allowed us to spend near no time on design. It works on mobile devices and doesn't look terrible. None of us are great designers, so we figured this was well worth it for the first release.
  • Not over thinking user management. Over time we plan to add accounts to the site so you can see your existing reservations from a dashboard, however for this first version we opted to go with a simple "email" to link together accounts. Users submit their existing hotel reservation with their email which we use later if you need to retrieve it. From there you can retrieve your "active" (reservations that have not yet past) reservations in an email we email to you.
  • Payment processing. This one was a no-brainer for us. We wanted to be PCI compliant and also have a good user experience. Stripe we had worked with in the past and knew it was incredibly easy to use. We went with the checkout feature so we never would have any of their credit card information and it never hit our servers, making us PCI compliant.

We also wanted to get feedback from a small group of users. We posted it on HackerNews and immediately started getting great feedback. We knew posting this on the day we were traveling for the Holidays wasn’t optimal as we couldn’t respond to feedback immediately, we wanted to get this launched. We managed to make it to the front page of HackerNews for a while and instantly had 2,500 unique visitors that day, up from zero the day before! The feedback was great the main points were:

  • Everyone loved the idea and thought if executed properly it'd be great!
  • People didn't like that we wanted to charge $19.99 regardless of if we could find a lower cost reservation. It was too risky.
  • Some of the design could use some love.
  • Pricing would be more interesting/better if it was a percentage saved or a money back guarantee.

Today we revamped our pricing strategy after the feedback. We knew the upfront cost was most likely a turn away for many users but didn’t know what percentage would hate it. After reading the feedback on the post and numerous emails, we’ve switched to a 20% of the amount saved. This makes it 100% risk free to the user. We won’t make money unless you save money. If we save you $100 dollars, you get $80 of it. We’ll be next week working on promoting the revised pricing strategy to see what additional feedback we can get as well as addressing the other parts of the feedback.

We’ll be trying to keep everyone updated on our adventures of launching our own product in house. We’re excited to try some techniques we’ve seen over the years and testing them out ourselves as well as trying some new ideas. If you have any feedback let us know!

Symfony2: Making impersonating a user more friendly

With Symfony2 the firewall comes with a built in feature: impersonate a user. We’ve been using impersonation as an admin tool for about 5 years as it is very effective for troubleshooting. When a user files a support ticket saying something isn’t showing properly to them or they are getting random errors it is very easy to just quickly switch to that user and see what they are seeing. As with all features, this one may not be appropriate for your application if your user expects no administrative staff to have access to his or her account.

While Symfony’s built in impersonation feature is a great step up from having to build it by hand, it still can be a bit more friendly. We’ve seen two additional functions we wanted the impersonation to handle. First, we wanted it to on exit from impersonating the user returns the user to where the user first started to impersonating. Currently it just brings you back to wherever you link the user. Second, if already impersonating a user and trying to start to impersonate another, we didn’t want it to throw an error but to quietly switch you. This functionality could lead to unwanted circumstances if an impersonating user believes they can impersonate another user, and then slowly just keep exiting impersonation of each user and go back up the chain they went down. However, in our situation the time admins hit this was when they’d impersonate one user, realize they clicked the wrong one, click back and try to impersonate a different user. As the browser uses it’s cached page when the user hits back they see the list of users as if they were an admin and can click on the correct user. If they do this they are hit with a 500 error, “You are already switched to X user”.

For both of our goals we overrode the built in switch user class. It is really easy to override, as all you need to do is specify in your parameters.yml “security.authentication.switchuser_listener.class: My\AppBundle\Listener\SwitchUser”. We used the built in class as our starting template: https://github.com/symfony/symfony/blob/2.5/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php Our final class ended looking like:

<?php
namespace My\AppBundle\Listener;

use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Role\SwitchUserRole;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Based on built in Symfony 2 SwitchUserListener class.  Modified to allow:
 * 1. Redirect user on exit back to original impersonation url if one exists
 * 2. Allow user to impersonate different user if they are already impersonating a user.
 */
class SwitchUserListener implements ListenerInterface
{
    private $securityContext;
    private $provider;
    private $userChecker;
    private $providerKey;
    private $accessDecisionManager;
    private $usernameParameter;
    private $role;
    private $logger;
    private $dispatcher;

    // Used to disable the URI redirect in case user is already impersonating one user and trying to switch to another.
    private $useOverrideUri;

    /**
     * Constructor.
     */
    public function __construct(SecurityContextInterface $securityContext, UserProviderInterface $provider, UserCheckerInterface $userChecker, $providerKey, AccessDecisionManagerInterface $accessDecisionManager, LoggerInterface $logger = null, $usernameParameter = '_switch_user', $role = 'ROLE_ALLOWED_TO_SWITCH', EventDispatcherInterface $dispatcher = null)
    {
        if (empty($providerKey)) {
            throw new \InvalidArgumentException('$providerKey must not be empty.');
        }

        $this->securityContext = $securityContext;
        $this->provider = $provider;
        $this->userChecker = $userChecker;
        $this->providerKey = $providerKey;
        $this->accessDecisionManager = $accessDecisionManager;
        $this->usernameParameter = $usernameParameter;
        $this->role = $role;
        $this->logger = $logger;
        $this->dispatcher = $dispatcher;
        $this->useOverrideUri = true;
    }

    /**
     * Handles the switch to another user.
     *
     * @param GetResponseEvent $event A GetResponseEvent instance
     *
     * @throws \LogicException if switching to a user failed
     */
    public function handle(GetResponseEvent $event)
    {
        $request = $event->getRequest();

        if (!$request->get($this->usernameParameter)) {
            return;
        }

        if ('_exit' === $request->get($this->usernameParameter)) {
            $this->securityContext->setToken($this->attemptExitUser($request));
        } else {
            try {
                $this->securityContext->setToken($this->attemptSwitchUser($request));
            } catch (AuthenticationException $e) {
                throw new \LogicException(sprintf('Switch User failed: "%s"', $e->getMessage()));
            }
        }

        $session = $request->getSession();

        $request->query->remove($this->usernameParameter);

        $overrideUri = $session->get('onSwitchURI',null);
        if($request->get('returnTo'))
        {

            $session->set('onSwitchURI',$request->get('returnTo'));
            $request->query->remove('returnTo');

        }
        else
            $session->remove('onSwitchURI');

        $request->server->set('QUERY_STRING', http_build_query($request->query->all()));


        $response = new RedirectResponse($this->useOverrideUri && $overrideUri ? $overrideUri : $request->getUri(), 302);

        $event->setResponse($response);
    }


    /**
     * Attempts to switch to another user.
     *
     * @param Request $request A Request instance
     *
     * @return TokenInterface|null The new TokenInterface if successfully switched, null otherwise
     *
     * @throws \LogicException
     * @throws AccessDeniedException
     */
    private function attemptSwitchUser(Request $request)
    {
        $token = $this->securityContext->getToken();
        $originalToken = $this->getOriginalToken($token);


        if (false !== $originalToken) {
            if ($token->getUsername() === $request->get($this->usernameParameter)) {
                return $token;
            } else {
                // User is impersonating someone, they are trying to switch directly to another user, make sure original user has access.
                if (false === $this->accessDecisionManager->decide($originalToken, [$this->role])){
                    throw new AccessDeniedException();
                }

                // User has a return url, most likely to admin area, as they are just trying to reimpersonate non-admin redirect to default.
                $this->useOverrideUri = false;

            }
        }
        else if (false === $this->accessDecisionManager->decide($token, [$this->role])) {
            throw new AccessDeniedException();
        }

        $username = $request->get($this->usernameParameter);

        if (null !== $this->logger) {
            $this->logger->info(sprintf('Attempt to switch to user "%s"', $username));
        }

        $user = $this->provider->loadUserByUsername($username);
        $this->userChecker->checkPostAuth($user);

        $roles = $user->getRoles();

        // If there is an original token, only let them switch back to that user.
        if($originalToken)
            $roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $originalToken);
        else
            $roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $this->securityContext->getToken());

        $token = new UsernamePasswordToken($user, $user->getPassword(), $this->providerKey, $roles);

        if (null !== $this->dispatcher) {
            $switchEvent = new SwitchUserEvent($request, $token->getUser());
            $this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
        }

        return $token;
    }

    /**
     * Attempts to exit from an already switched user.
     *
     * @param Request $request A Request instance
     *
     * @return TokenInterface The original TokenInterface instance
     *
     * @throws AuthenticationCredentialsNotFoundException
     */
    private function attemptExitUser(Request $request)
    {
        if (false === $original = $this->getOriginalToken($this->securityContext->getToken())) {
            throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
        }

        if (null !== $this->dispatcher) {
            $switchEvent = new SwitchUserEvent($request, $original->getUser());
            $this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
        }

        return $original;
    }

    /**
     * Gets the original Token from a switched one.
     *
     * @param TokenInterface $token A switched TokenInterface instance
     *
     * @return TokenInterface|false The original TokenInterface instance, false if the current TokenInterface is not switched
     */
    private function getOriginalToken(TokenInterface $token)
    {
        foreach ($token->getRoles() as $role) {
            if ($role instanceof SwitchUserRole) {
                return $role->getSource();
            }
        }

        return false;
    }
}

Here are the specifics on what everything we did and why.

First feature: Redirecting the user on exiting impersonating a user to where they originally started impersonating them. As we didn’t want to go around our entire application updating logic for the exit impersonation links if we decided to later change the behavior, we decided to build the redirect into the class itself. We didn’t want to rely on the user’s browser referrer header, so instead we decided to on the links to impersonate a user to include a “returnTo” parameter. This parameter is set to the current URI (app.request.uri). At line 97 we save the returnTo parameter to the session, for later use. On line 93, as a user is switching (in this case exiting) a user, if the session has a stored “returnTo” URL, we assign it to the “$overrideURI” variable. On line 107 we have a bit of logic on if we redirect them to the default route or the “returnTo” URL. The reason for the additional “$this->useOverrideURI” variable on this line is for our second feature of switching between users when you are already impersonating one. As the logic all runs through the same routine, if you are simply switching to a new user from an already impersonated one, we don’t want to redirect you back to your original URL when you started all the impersonating, so we disregard the redirect in this case and redirect to the default route. An example of this is admin impersonates user A, then wants to impersonate user B. Upon impersonating user B, the admin does not want to be redirected back to the admin dashboard (the sessions returnTo URL), but to where the impersonate user link is pointing to (User B homepage).

Second feature: Allow users to impersonate a different user while already impersonating another. One Line 134 is where the original SwitchUserListener would usually throw a 500 error as you are already impersonating a user. Instead, we make sure that the original token has the appropriate permissions, if so it will not throw an exception. Line 159 is the other main update for this feature. If you are already impersonating a user and try to impersonate another user, upon exiting you want to go back to your original user. Now if a original impersonation token (user) exists, we keep that as the user you’ll be switched to when you exit the impersonation.

Symfony2: Outputting form checkboxes in a hierarchy

Recently when I was working on a client project we had a bunch of permissions which had a hierarchy (or tree structure). For example, you needed Permission 1 to have Permission 1a and Permission 1b. In the examples below lets assume $choices is equal to the following:

<?php
$choices = [
    'Permission 1' => 'Permission 1',
    'Permission 1 Sub-Permissions' => [
	'Permission 1a' => '1a',
	'Permission 1b' => '1b'
    ],
    'Permission 2' => 'Permission 2',
    'Permission 3' => 'Permission 3',
    'Permission 3 Sub-Permissions' => [
	'Permission 3a' => '3a',
	'Permission 3b' => '3b'
    ]
];

At first, I used the built in in optgroups of a the select box to output the form, so it was clear what permissions fell where. My form would look similar to:

<?php
class Form extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('permissions','choice',['choices' => $choices, 'multiple' => true]);
    }

}

Multiple select boxes aren’t the easiest to work with as we all know. Also, it isn’t as easy to visually see the difference as the height of the select box could not be long enough to show you what an optgroup’s title is. Instead, I decided to use the checkbox approach. Issue with this, the current Symfony2 form themes don’t output checkboxes in groups or with any visual indication of the hierarchy. I ended up creating my own custom field type so I could customize the way it renders globally via the form themeing. My custom type just always set the choice options to expanded and multiple as true. For the actual rendering, below is what I ended up with.

{% block checkbox_hierarchy_widget %}
  <ul class="listless spaced-listless" id="mainList">
        {% for choiceOrOptionGroup,children in form.vars.choices %}
            {% if children is iterable %}
                <ul>
                    {% for child,choiceView in children %}
                        <li>
                            <label class="checkbox">
                                {{ form_widget(form.offsetGet(child)) }}
                                {{ form.offsetGet(child).vars.label }}
                            </label>
                        </li>
                    {% endfor %}
                </ul>
            {% else %}

                {# If not first loop, close previous <li> #}
                {% if not loop.first %}
                    </li>
                {% endif %}

                <li>
                <label class="checkbox">
                    {{ form_widget(form.offsetGet(choiceOrOptionGroup)) }}
                    {{ form.offsetGet(choiceOrOptionGroup).vars.label }}
                </label>
            {% endif %}

            {# Last of the loop, there will be an open <li>, close it. #}
            {% if loop.last %}
                </li>
            {% endif %}
        {% endfor %}
    </ul>

    <script type="text/javascript">
        $("#mainList").children('li').find('input:first').on('change',function(e,isPageLoad){

            var children = $(this).parents('li:first').find('ul').find('input');
            if($(this).is(':checked'))
            {
                children.prop('disabled',false);
                if(!isPageLoad)
                    children.prop('checked',true);
            }
            else
            {
                children.prop('disabled',true).prop('checked',false);
            }
        }).trigger('change',[true]);
    </script>
{% endblock %}

The above is assuming you are using bootstrap to render your forms as it has those classes. My listless class just sets the ul list style to none. The code should be fairly easy to follow, basically it goes through and any sub-array (an optgroup) it will nest in the list from the previous option. This method does assume that you have the ‘parent’ node before the nested array. I also in the bottom have some javascript that basically makes sure that you can’t check off a sub-group if the parent is not checked. When you first check the parent, it selects all the children. For the example I just put the javascript in there, it uses and id attribute, so you can only have one of these per page. If you were using this globally, I’d recommend tagging the UL with a data attribute and moving the javascript into a global JS file.

Since a picture is worth a thousand words, here is an example of what it looks like working:

permissions-example

Let me know if you have any questions! Happy Friday.

Symfony2: Using "request_matcher" for custom firewall rules

Last week we were looking to leverage a set of JSON API endpoints in a Symfony2 project to power a single page Javascript app. The way the API had been setup, the routes were all secured with an HTTP Basic Auth firewall matching on “/api”. This worked great for the mobile apps but for a Javascript app it would be awkward to have the user re-enter their credentials to authorize the basic auth firewall. What we really wanted to do was to leave everything “as is” but have Symfony use the normal cookie based firewall when we passed in a special “isOnlineApp” parameters on the URL.

Unfortunately, setting something like this up with the default “pattern” setting in your security.yml file isn’t possible. The “pattern” setting only matches on the route URL, not the parameters so there’s no way to have it selectively trigger when a parameter is present on a URL. So how do you do it? Well as it turns out, there’s a firewall configuration called “reuqest_matcher” which lets you “match” a firewall using a service. Just create a service that extends the RequestMatcherInterface, implment a “matches” function, and then add the class as a service.

Our code for the service ended up looking like:

<?php

namespace SF\AppBundle\Security;

use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\Request;

class ApiFirewallMatcher implements RequestMatcherInterface
{

    public function matches(Request $request){
        $url = $request->getPathInfo();
        $isMatch = strpos($url, "/api") === 0 && $request->get("isOnlineApp", false) === false;
        return $isMatch;
    }

}

And then the actual firewall configuration ends up being:

    firewalls:
        api_basic_auth:
            request_matcher: ct.security.api_firewall_matcher
            provider: fos_userbundle
            form_login: false
            stateless: true
            anonymous: ~
            http_basic:
                realm: "API"
        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_provider: form.csrf_provider
                check_path:     fos_user_security_check
                target_path_parameter:          _target_path
                
            logout:
                target: /login
            anonymous:    true

You don’t need a “pattern” setting anymore since the “matches” function supersedes it. Anyway, let me know if you have any questions!