Simple User Management in Silex

This article shows an easy way to set up user accounts (authentication, authorization, and user administration) in the Silex PHP micro-framework, by employing the Silex SimpleUser library.

To just hit the ground running, see the quick start instructions in the SimpleUser README, and check out the source code of the online demo (especially src/app.php).

For a walkthrough of how it works and what you can do with it, read on.

Contents:
[—ATOC—]
[—TAG:h2—]

How Silex handles user authentication

Silex has built-in support for the Symfony 2 Security component, a comprehensive security system for web applications which handles user authentication and authorization. See the Silex SecurityServiceProvider documentation and the Security section of the Symfony 2 Book for details about how it all works.

The Security component is powerful, but it’s also complex. Configuration can be confusing at first. And it requires a fair amount of custom code to be written: you need to create a “user provider” (a class for storing and retrieving user objects), and you need to write controllers and views for logging in, registering, administering accounts, etc.

If your needs are simple, or if you’re just getting started, all of this can seem like overkill. To get up and running more easily, employ the SimpleUser library.

Introducing Silex SimpleUser

Silex SimpleUser provides drop-in services for Silex that implement the missing user management pieces for the Security component. It includes a basic User model, a database-backed user manager, controllers and views for user administration, and various supporting features.

If your Silex application just needs a user authentication layer with a minimal user model, SimpleUser may work fine for you out of the box.

If you need to add more fields to the User model or change the design of the user pages, you can use custom fields and custom view templates. If your needs are more complex, you can extend the SimpleUser classes, or just treat them as a reference implementation to build your own services.

Installing SimpleUser

Install the silex-simpleuser package with Composer by running the following command:

composer require jasongrimes/silex-simpleuser

Create the database tables using one of the SQL files available in vendor/jasongrimes/silex-simpleuser/sql. If you’re using MySQL, do it like this:

mysql -uUSER -pPASSWORD MYDBNAME < vendor/jasongrimes/silex-simpleuser/sql/mysql.sql

SimpleUser depends on the DoctrineServiceProvider , which provides a database abstraction layer called Doctrine DBAL (it does not include the full Doctrine ORM).

In addition, the optional controllers provided by SimpleUser for handling form-based authentication and user management depend on the SessionService ControllerUrl GeneratorTwig, and Swiftmailer service providers.

Composer takes care of downloading all of these dependencies.

Register the security service provider like this:

use Silex\Provider;

$app->register(new Provider\SecurityServiceProvider());

Enable Doctrine something like this:

$app->register(new Provider\DoctrineServiceProvider());

$app['db.options'] = array(
    'driver'   => 'pdo_mysql',
    'host'     => 'localhost',
    'dbname'   => 'MY_DBNAME',
    'user'     => 'MY_DB_USER',
    'password' => 'MY_DB_PASSWORD',
);

Register the SimpleUser\UserServiceProvider in your Silex application:

$simpleUserProvider = new SimpleUser\UserServiceProvider();
$app->register($simpleUserProvider);

The following services will now be available:

  • $app['user.manager']: A service for managing User objects. It’s an instance of SimpleUser\UserManager.
  • $app['user']: A SimpleUser\User instance representing the currently authenticated user (or null if the user is not logged in).
  • $app['user.controller']: A controller with actions for handling user management routes.
  • $app['user.tokenGenerator']: A SimpleUser\TokenGenerator instance for generating secure authentication tokens.
  • $app['user.last_auth_exception']: A convenience function for accessing an authentication exception thrown by the Security system (which can otherwise be really tricky to catch). Call it like this:
    $exception = $app['user.last_auth_exception']($request);

To configure the Silex security service to use the SimpleUser\UserManager as its user provider, add it to your security.firewalls configuration like this:

$app->register(new Provider\SecurityServiceProvider());

$app['security.firewalls'] = array(
    'secured_area' => array(
        'users' => $app->share(function($app) { return $app['user.manager']; }),
        // ...
    ),
);

The rest of the firewall configuration depends on the needs of your application. See below for details on configuring it for form-based authentication with the built-in SimpleUser controllers.

Setting up routes and controllers (optional)

SimpleUser provides some built-in routes and controllers that can be used for logging in and managing users. They’re defined in SimpleUser\UserController.

You don’t have to use these built-in controllers, it’s entirely optional. You could write your own instead, or you could use SimpleUser with an alternate authentication method that doesn’t need them (like HTTP basic authentication or OAuth2).

To use SimpleUser’s built-in routes and controllers, first register these additional services:

$app->register(new Provider\RememberMeServiceProvider());
$app->register(new Provider\SessionServiceProvider());
$app->register(new Provider\ServiceControllerServiceProvider());
$app->register(new Provider\UrlGeneratorServiceProvider());
$app->register(new Provider\TwigServiceProvider());
$app->register(new Provider\SwiftmailerServiceProvider());

Then mount the SimpleUser routes and controllers like this:

$userServiceProvider = new SimpleUser\UserServiceProvider();
$app->register($userServiceProvider);

// Mount SimpleUser routes.
$app->mount('/user', $userServiceProvider);

The following routes are provided. (In this example they are mounted under /user, but that can be changed by altering the argument to mount() above.)

Route path Route name
/user/login user.login The login form.
/user/login_check user.login_check Process the login submission. The login form POSTs here.
/user/forgot-password user.forgot-pasword Initiate a password reset request.
/user/reset-password/{token} user.reset-password Reset a user’s password. Arrived at from a special link sent via email.
/user/logout user.logout Log out the current user.
/user/register user.register Form to create a new user.
/user/confirm-email/{token} user.confirm-email Activate a new account after verifying its email address (optional).
/user user View the profile of the current user.
/user/{id} user.view View a user profile.
/user/{id}/edit user.edit Edit a user.
/user/list user.list List users.

Configure the firewall to use these routes for form-based authentication. (Replace “/user” with whatever mount point you used with mount() above).


$app['security.firewalls'] = array(
    'secured_area' => array(
        'pattern' => '^.*$',
        'anonymous' => true,
        'remember_me' => array(),
        'form' => array(
            'login_path' => '/user/login',
            'check_path' => '/user/login_check',
        ),
        'logout' => array(
            'logout_path' => '/user/logout',
        ),
        'users' => $app->share(function($app) { return $app['user.manager']; }),
    ),
);

Note: if you want to set 'anonymous' => false  (to require authentication in order to access the secured area), you need to make sure the login path still allows anonymous access. You can do that by defining an empty firewall for that path, before the secured_area firewall, like this:

$app['security.firewalls'] = array(
    // Ensure that the login page is accessible to all
    'login' => array(
        'pattern' => '^/user/login$',
    ),
    'secured_area' => array(
        'pattern' => '^.*$',
        'anonymous' => false,
        // ...
    ),
);

Overriding view templates

Even if you want to use the built-in view templates for the user administration pages, you’ll almost certainly want to customize at least the base template that provides the page layout. By default, this layout template is set to @user/layout.twig, stored in src/SimpleUser/views/layout.twig.

Create your own Twig layout template, copying and pasting as necessary from the default template. Configure Twig to look for templates in the directory you stored it in, ex. like this:

$app['twig.path'] = array(__DIR__.'/../templates');

Then configure SimpleUser to use your new layout template, specifying the relative path to the template file:

$app['user.options'] = array(
    // ...
    'templates' => array(
        'layout' => 'layout.twig',
    ),
);

You can override any of the other view templates as well:

$app['user.options'] = array(
    // ...
    'templates' => array(
        'layout' => 'layout.twig',
        'register' => 'register.twig',
        'register-confirmation-sent' => 'register-confirmation-sent.twig',
        'login' => 'login.twig',
        'login-confirmation-needed' => 'login-confirmation-needed.twig',
        'forgot-password' => 'forgot-password.twig',
        'reset-password' => 'reset-password.twig',
        'view' => 'view.twig',
        'edit' => 'edit.twig',
        'list' => 'list.twig',
    ),
);

Access control

The Symfony Security component uses “attributes” to specify the rights a given user should have. Attributes can refer to roles, like “ROLE_ADMIN” or “ROLE_USER”, or they can refer to specific permissions, like “EDIT_USER”. To control access in your code, test whether a user has a given attribute using the isGranted() method:

if ($app['security']->isGranted('ROLE_ADMIN')) { ... }

Or, in a Twig template:

{% if is_granted('ROLE_ADMIN') %} ... {% endif %]

To create your first admin user, you can create a regular user account in the web interface. Then manually assign the ROLE_ADMIN role to that user in the database, by setting the users.roles value to ROLE_USER,ROLE_ADMIN.  After you have one admin account, it can grant the admin role to others via the web interface.

Alternately, you can create an admin account with the user manager:

$user = $app['user.manager']->createUser('test@example.com', 'MySeCrEtPaSsWoRd', 'John Doe', array('ROLE_ADMIN'));
$app['user.manager']->insert($user);

The SimpleUser\UserServiceProvider sets up custom access control attributes for testing whether the viewer can edit a user.

  • EDIT_USER: Whether the current user is allowed to edit the given user object.
  • EDIT_USER_ID: Whether the currently authenticated user is allowed to edit the user with the given user ID. Useful for controlling access in before() middlewares.

By default, users can edit their own user account, and those with ROLE_ADMIN can edit any user. To change these privileges, override  SimpleUser\EditUserVoter.

Inside of a controller, test privileges like this:

// Test if the viewer has access to edit the given $user
if ($app['security']->isGranted('EDIT_USER', $user)) { ... }

// You can also test access by user ID without instantiating a User, ex. in a before() middleware
$app->post('/user/{id}/edit', 'user.controller:editAction')
    ->before(function(Request $request) use ($app) {
        if (!$app['security']->isGranted('EDIT_USER_ID', $request->get('id')) {
            throw new AccessDeniedException();
        }
    });

In a Twig template, test privileges like this:

{% if is_granted('EDIT_USER', user) %}
...
{% endif %}

The UserManager

SimpleUser\UserManager is a service for managing User objects. It can be accessed as $app['user.manager'].

Here are some of its more useful methods:

getUser($id)
Get a User instance from the database by its ID.

findBy($criteria = array(), $options = array())
Get a list of users matching the given criteria. Examples:

// Get a list of users with email "test@example.com"
$users = $app['user.manager']->findBy(array(
    'email' => 'test@example.com',
));

// Get the first 5 in the list of all users, ordered by name
$users = $app['user.manager']->findBy(array(), array(
    'order_by' => 'name',
    'limit' => 5,
));

// Example of sorting in descending order
// and using a limit with an offset
$users = $app['user.manager']->findBy(array(), array(
    'order_by' => array('time_created', 'DESC'),
    'limit' => array($offset, $limit),
));

findOneBy($criteria = array(), $options = array())
Like findBy(), but returns only the first matching User object instead of an array.

createUser($email, $plainPassword, $name = null, $roles = array())
A factory method for creating a new User object.

insert(User $user)
Insert a new user instance into the database.

update(User $user)
Update data in the database for an existing user.

delete(User $user)
Delete a user from the database.

Usernames

Usernames are not used by default; users sign in by their email address instead. Usernames can be enabled as a config option, though, in which case users can sign in by username or email.

To require User instances to have usernames, set the following config option:

$app['user.options'] = array(
    // ...
    'isUsernameRequired' => true,
);

This causes the registration and editing forms to include a “username” field, and causes validation to fail if a User’s username is not set.

An implementation detail is worth noting here. Because usernames are optional by default, and because the Symfony Security component expects that the getUsername() method will always return a value, SimpleUser\User::getUsername() will return the email address if the username is empty. To access the real username value (or null if it’s unset), use getRealUsername() instead.

Password strength

By default, SimpleUser allows passwords to be pretty much anything. To implement custom password strength requirements, override the  user.passwordStrengthValidator service. This service is used by the register, reset-password, and edit controllers to test whether new passwords are strong enough.

The password strength validator should be a function that takes a User instance and a plain-text password as arguments, and returns an error string if the password isn’t strong enough (or null otherwise). For example:

// Example of defining a custom password strength validator.
// Must return an error string on failure, and void or null on success.
$app['user.passwordStrengthValidator'] = $app->protect(function(SimpleUser\User $user, $password) {
    if (strlen($password) < 8) {
        return 'Password must be at least 8 characters long.';
    }
    if (strtolower($password) == strtolower($user->getName())) {
        return 'Your password cannot be the same as your name.';
    }
});

Email confirmation and password reset

If the SwiftmailerServiceProvider is registered, SimpleUser supports sending emails for two features: resetting passwords, and optionally requiring email confirmation before activating new user accounts.

By default, emails will be sent from “do-not-reply@<HOSTNAME>”.  To specify a different from address, set the following options:

$app['user.options'] = array(
    // ...
    'mailer' => array(
        'fromEmail' => array(
            'address' => 'you@yourdomain.com',
            'name' => 'Your Organization',
        ),
    ),
);

New accounts do not require email confirmation by default. To enable this feature, set the following option:

$app['user.options'] = array(
    // ...
    'emailConfirmation' => array(
        'required' => true,
    ),
);

The email messages are rendered from Twig templates, as both plain text and HTML messages. The default templates are stored in src/SimpleUser/views/email/. To use your own custom templates instead, specify the following config options:

$app['user.options'] = array(
    // ...
    'emailConfirmation' => array(
        'required' => true,
        'template' => 'email/confirm-email.twig',
    ),
    'passwordReset' => array(
        'template' => 'email/reset-password.twig',
    ),
);

Custom fields

The SimpleUser\User class is pretty minimal. It basically consists of an email, password, name, and user ID. If you need to store additional values in a user object, you can add them as custom fields.

$user = $app['user.manager']->getUser($id);

// Set custom field "foo" to the value "bar".
$user->setCustomField('foo', 'bar');

$val = $user->getCustomField('foo');

// Get a list of users with custom field "foo" equal to "bar".
$users = $app['user.manager']->findBy(array(
    'customFields' => array('foo' => 'bar')
));

Access custom fields in a twig template like this:

{% if user.hasCustomField('twitterUsername') %}
    <a href="http://twitter.com/{{ user.getCustomField('twitterUsername') }}">
        {{ user.getCustomField('twitterUsername') }}
    </a>
{% endif %}

You can make your custom fields editable by the built in “edit user” controller by setting the following config options:

$app['user.options'] = array(
    'controllers' => array(
        'edit' => array(
            'customFields' = array(
                'twitterUsername' => 'Twitter username',
             ),
         ),
    ),
);

This will cause the edit user controller to look for a request parameter named “twitterUsername”, and save it as a custom field with the same name. The default edit template will also include a text field for “Twitter username” (or you can provide a custom edit template if you prefer).

Note that this approach just stores text as the user enters it. It doesn’t do any validation, or handle fields that aren’t plain text. To support that, you can create a custom User class.

Overriding the User class

For more flexibility, you can create your own user class. As long as it extends SimpleUser\User, and stores additional properties internally as custom fields, you can still use the existing UserManager and controllers.

Here’s an example custom user class that adds a “twitterUsername” property, and validation for it.

namespace Demo;

use SimpleUser\User as BaseUser;

class User extends BaseUser
{
    public function __construct($email)
    {
        parent::__construct($email);
    }

    public function getTwitterUsername()
    {
        return $this->getCustomField('twitterUsername');
    }

    public function setTwitterUsername($twitterUsername)
    {
        $this->setCustomField('twitterUsername', $twitterUsername);
    }

    public function validate()
    {
        $errors = parent::validate();

        if ($this->getTwitterUsername() && strpos($this->getTwitterUsername(), '@') !== 0) {
            $errors['twitterUsername'] = 'Twitter username must begin with @.';
        }

        return $errors;
    }
}

Then configure the UserManager to return users as instances of your custom class:

$app['user.options'] = array(
    'userClass' => '\Demo\User',
);

Now you can access the twitterUsername property directly in a Twig template, instead of going through the getCustomField() method:

<p>
    Twitter username: {{ user.twitterUsername }}
</p>

Overriding the UserManager and other services

To customize the UserManager, create your own class that extends SimpleUser\UserManager.  Then simply override the user.manager key in the Silex service container after registering the UserServiceProvider.

For example, you could put something like this in your src/app.php:

$app->register(new SimpleUser\UserServiceProvider());

$app['user.manager'] = $app->share(function($app) {
    return new MyUserManager(...);
});

This approach works for overriding any of the other SimpleUser services as well. See the Silex Services documentation for details about defining services and using the service container.

Overriding individual routes and controllers

To override an individual controller, simply define your own version of the route before mounting the controller provider. For example:

// Register the SimpleUser service provider.
$simpleUserProvider = new SimpleUser\UserServiceProvider();
$app->register($simpleUserProvider);

// Override controllers by defining the same route
// *before* mounting the controller provider.
$app->method('GET|POST')->match('/user/register', function(Application $app) {
    return new Response('Sorry, registration has been disabled.');
});

// Mount the controller provider.
$app->mount('/user', $simpleUserProvider);

Refer to the original route definitions in the connect() method of SimpleUser\UserServiceProvider, and the matching controller definition in SimpleUser\UserController.

Events

To make it easier to customize behavior, the UserManager fires events using the Symfony EventDispatcher (included with the core Silex components). Add listeners to the EventDispatcher to execute your own custom code when these events fire.

For example, to write a log message when a new user is created, listen for the UserEvents::AFTER_INSERT event, like this:

use SimpleUser\UserEvents;

$app['dispatcher']->addListener(UserEvents::AFTER_INSERT, function(UserEvent $event) use ($app) {
    $user = $event->getUser();
    $app['logger']->info('Created user ' . $user->getId());
});

Each event makes the relevant User instance available via its getUser() method.

See a list of all supported events in the static UserEvents class.

More events will be added as needed. Just ask if you need an event thrown at another point in the User lifecycle, or feel free to send a pull request.

Feedback is welcome

I hope this makes it easier for you to set up user authentication in Silex. Any feedback on this article or on the SimpleUser library is welcome.

To report bugs or request new features, please post to the SimpleUser Issues page on Github.

If you haven’t already, check out the the online demo and its source code for a fully functional example, especially src/app.php.

13 thoughts on “Simple User Management in Silex”

  1. Many thanks for posting this Jason. I have been using an extended version of SimpleUser in the authentication for a website and this has made a few points about the use of the authentication a lot clearer and cleared up a few issues for me like substituting my own templates. I had achieved that by redirecting the path to the template files but the above is a neater cleaner way to do it. For my purposes I also had to extend the User Service Provider, UserManager and User Controller but I may be able to revisit that.
    Cheers
    David

  2. i have following error when i try to run this Simple User demo on My Machine
    Fatal error: Class ‘SimpleUser\UserServiceProvider’ not found in C:\wamp\www\silex\src\app.php on line 25

  3. problem installing:
    "Your requirements could not be resolved to an installable set of packages."
    Problem 1

    - jasongrimes/silex-simpleuser 2.0.1 requires doctrine/dbal ~2.4 -> no matching package found.

    - jasongrimes/silex-simpleuser 2.0 requires doctrine/dbal ~2.4 -> no matching package found.

    - Installation request for jasongrimes/silex-simpleuser ~2.0 -> satisfiable by jasongrimes/silex-simpleuser[2.0, 2.0.1].

  4. Hi Jason,

    I opened an issue to the github repo and it was about adding recaptcha for anti spam on user registration. I found out the solution by using the overriding the route in the controller using a middleware.

    Great tutorial and overall your library was a great help indeed.

  5. Hi,
    I’m trying to override the controllers and the templates but I didn’t succeed so far.

    The example you posted for the controller is seems to be wrong as $app doesn’t have a method named “method”. In your connect method you used $controller = $controllers = $app[‘controllers_factory’]; which has the “method” method.

    Can you provide a little help?

    1. I’m trying doing it like this:
      $userServiceProvider = new SimpleUser\UserServiceProvider();
      $app->register($userServiceProvider);

      $app->match(‘/list’,
      function(\Aptoma\Silex\Application $app) {
      return new \Symfony\Component\HttpFoundation\Response(‘Sorry, only for admin.’);
      });

      // Mount SimpleUser routes.
      $app->mount(‘/’, $userServiceProvider);

  6. Awesome! Just what I needed. I was wondering how this could integrate with Twitter / other social logins too? Any ideas would be a great help.

  7. Hi,
    thanks for the tutorial. But i have a problem with custom fields. I have added two custom fields like this:
    $app[‘user.options’] = array(
    ‘userClass’ => ‘VibeSMS\Model\User’,
    ‘editCustomFields’ => array(
    ‘firstName’ => ‘First name’,
    ‘lastName’ => ‘Last name’,
    ),
    ….
    );
    I have also added getters and setters as mentioned above
    in my VibeSMS\Model\User extends SimpleUser\User and also edited register.twig adding two text fields (firstName and lastName) but when i register an user through /user/register the app doesn’t store my custom fields value in the database but it works in the /user/{id}/edit. Any idea o suggestion which can help ??

  8. Same problem here:

    At event: UserEvents::AFTER_INSERT
    $user->setCustomField(‘key’, $value);

    As I debugged it, the $User class it setting the value but not making this change permanent. To do so, use the following:

    $userManager = $app[‘user.manager’];
    $userManager->update($user)

    where $user = $event->getUser();
    Hope that helps. Great work, thank you very much!

  9. Hi,
    very nice library, thanks for that!

    One little question:
    How do I know that the form-field-name for the username has to be “_username”?
    Is this hardcoded somewhere or can I change this?
    Thanks!

  10. Hi Jason,
    Is there any conflict between the following forms for configuring the custom fields.

    $app[‘user.options’] = array(
    // A list of custom fields to support in the edit controller.
    ‘editCustomFields’ => array(),
    );

    You can make your custom fields editable by the built in “edit user” controller by setting the following config options:

    $app[‘user.options’] = array(
    ‘controllers’ => array(
    ‘edit’ => array(
    ‘customFields’ = array(
    ‘twitterUsername’ => ‘Twitter username’,
    ),
    ),
    ),
    );

    I am having a problem trying to define validation of the custom fields in a custom User class extended from the SimpleUser\User class. when the $user->validate() method is called, the custom fields are not defined in the $user instance. The $customFields array in the User instance is null. I cannot find in theuserController::createUserFromRequest(..) where the custom fields were obtained from the request and wondered whether the configuration was part of the problem.

    Cheers

    David Cousens

    1. After digging around in the userController the createUserFromRequest() method called by the does not fetch the custom Fields from the request whereas the editAction ‘POST method after editing the fields does with the lines

      $customFields = $this->editCustomFields ?: array();
      foreach (array_keys($customFields) as $customField) {
      if ($request->request->has($customField)) {
      $user->setCustomField($customField, $request->request->get($customField));
      }
      }

      before calling this-> userManager->validate($user).

      I have extended the UserController to override the createUserFromRequest($request) and added these lines in the extended version. Next problem is to have the extended UserController supplied. in my bootstrap.php file when the UserServiceProvider is registered, I overrode the $app[‘user.controller as follows:

      use use PrintOz\User\UserController; // my extended controller.
      $app->register($app[‘user.provider’] = new SimpleUser\UserServiceProvider($app));
      // override the UserController in the SimpleUser
      $app[‘user.controller’] = $app->share(function ($app) {
      $app[‘user.options.init’]();

      $controller = new UserController($app[‘user.manager’]);
      $controller->setUsernameRequired($app[‘user.options’][‘isUsernameRequired’]);
      $controller->setEmailConfirmationRequired($app[‘user.options’][‘emailConfirmation’][‘required’]);
      $controller->setTemplates($app[‘user.options’][‘templates’]);
      $controller->setEditCustomFields($app[‘user.options’][‘editCustomFields’]);

      return $controller;
      });

      Not sure if this will work but giving it a try. The customFields should then be defined before validation, which means I can validate them in my extended User class (my original problem as i never got as far as inserting them and what was inserted was blankcustomFields in any case) and the call to Usermanager::insert($user) should then save them. Will raise this as an issue if this works as it will be better if incorporated into the SimpleUser\UserController. Will get back after debugging to confirm whether it worked

      David

Leave a Reply to fer Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>