Slow user creation because of password generation in fixtures

Slow user creation because of password generation in fixtures

Creating user for tests can be really time consuming due to the password hashing. This is no accident.

The hashing has to be slow. This is no bug, but a feature. Slow in creation also means slow to crack. Creating things like rainbow tables is way more expensive this way.

Especially in testing there are cases where this is an issue though. When working with FOSUser bundle for example the constructors require a password. And when you want to have 150 users for an end to end test where you want to load a lot of users in the frontend the fixture generation might take a few minutes.

The solution: Just create users without a password. I setup a special user provider helper for this.

<?php

declare(strict_types=1);

namespace App\DataFixtures\E2E;

use App\Entity\User;
use App\Repository\UserRepository;
use Faker\Factory;
use Faker\Generator;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;

final class UserProvider
{
    /** @var EncoderFactoryInterface */
    private $encoderFactory;

    /** @var UserRepository */
    private $userRepository;

    /** @var Generator */
    private $faker;

    public function __construct(
        EncoderFactoryInterface $encoderFactory,
        UserRepository $userRepository,
        string $hashAlgorithm
    ) {
        $this->encoderFactory = $encoderFactory;
        $this->userRepository = $userRepository;
        $this->faker = Factory::create();
    }

    /** Password generation takes a lot of time and should be done once in advance */
    public function generatePassword(string $plainPassword): string
    {
        $encoder = $this->encoderFactory->getEncoder(new User());

        return $encoder->encodePassword($plainPassword, null);
    }

    public function createUser(
        string $gender,
        string $email,
        string $generatedPassword,
        array $roles
    ): User {
        $user = $this->createBaseUser($gender, $email, $roles);

        $user->setEmailCanonical($email);
        $user->setUsernameCanonical($email);
        $user->setPassword($generatedPassword);

        $this->userRepository->store($user);

        return $user;
    }

    public function createUserWithoutPassword(
        string $gender,
        string $email,
        array $roles
    ): User {
        $user = $this->createBaseUser($gender, $email, $roles);

        $user->setUsernameCanonical($email);
        $user->setEmailCanonical($email);
        $user->setPassword('');
        $this->userRepository->store($user);

        return $user;
    }

    private function createBaseUser(string $gender, string $email, array $roles): User
    {
        $user = new User();
        $user->setGender($gender);
        $user->setFirstName($this->faker->firstName);
        $user->setLastName($this->faker->lastName);
        $user->setEmail($email);
        $user->setUsername($email);
        $user->setEnabled(true);
        foreach ($roles as $role) {
            $user->addRole($role);
        }

        return $user;
    }
}

There are multiple helpers in there.

$generatedPassword = generatePassword(...) generates a password from a plain password. This can be done once in an end to end test environment. This password can then be used to call createUser with the $generatedPassword. This way the slow password generation is just once.

For all instances where you don't need to login with the user (for example when showing a lot of users in a list) you can use createUserWithoutPassword(...) and not even generate a slow password once.