Do not (always) use FOSUserBundle

And what to consider before using it

Our journey

  1. What is FOSUserBundle
  2. Why it's not the right fit for a professional project
  3. Implementing your own User Manager
  4. Choosing a Bundle, the Right Way Β©

Hi there! πŸ‘‹

  • @damienalexandre πŸ‡«πŸ‡·
  • Symfony consultant since 2008
  • Contributor, writer, speaker
  • Elasticsearch πŸ”Ž and Unicode enthusiast
  • Beer 🍺 and Bike 🚲, not in that order
  • First time public speaking in English πŸ‡¬πŸ‡§

Me

I'm a consultant

I do audits, developments, trainings in Paris

JoliCode

We build web and mobile apps for awesome clients.

It all started with a new project

  • Install Symfony and FOSUserBundle πŸš€
  • Client wants some modifications
  • Tweak the bundle
  • Rewrite part of the bundle
  • Take a step back
  • "What the hell did I do" πŸ˜₯

One year ago

I had to let it go:

Article FOSUserBundle

Was ist FOSUserBundle?

A bit of history

  • May, 2010: Original idea and first implementation by KNPLabs πŸŽ‰
    • Was called DoctrineUserBundle in reference to our good old sfDoctrineGuardPlugin πŸ‘΄
  • December 2010: it was moved to FriendOfSymfony
    • FOS was born!

A bit of history

By then you had to use Git Submodule to install bundles:

git submodule add \
    git://github.com/knplabs/DoctrineUserBundle.git \
    src/Bundle/DoctrineUserBundle

😨

Symfony 2 release

July 2011: Version 1.0 was tagged,
1 month after Symfony 2.0 release.

Yes we had a deps file 😬

[FOSUserBundle]
git=git://github.com/FriendsOfSymfony/FOSUserBundle.git
target=bundles/FOS/UserBundle
  • πŸ‘ͺ 336 contributors
  • 🏷 20 releases
  • πŸ“‚ 5 700 000 downloads
  • πŸ“’ Documentation available on Symfony.com
  • πŸ‘ Recommended on the official documentation
  • πŸ… Most popular on KnpBundles
  • πŸ“ ~5000 lines of code (NCLOC: excluding the tests and comments)

All the features

  • Provide stored User object for Symfony projects:
    • Doctrine ORM
    • Doctrine ODM
    • Propel (removed from 2.0, new separated bundle)
  • Registration form with optional confirmation email
  • Profile editing form
  • Password reset form and handling
  • Console commands for users management

What is not in FOSUser

  • Authentication (Form login, basic, LDAP...)
  • Authorisation (Access controls...)
  • Session management
  • Remember Me
  • Impersonate User
  • Doctrine User Provider...

Two versions

  • 1.3.6: latest stable version (1.3.7 last week)
    • Not compatible with Symfony 3
    • 54k monthly downloads [source]
  • 2.0.0-alpha3: released 1 year ago (beta1 2 days ago)
    • Major update, lots of BC Break
    • 24k monthly downloads
  • dev-master has 135k monthly downloads 😱

By the way

Congrats @XWB and @stof for the two releases!

πŸŽ‰

Why you
should not
use it πŸ™Š

It's an implementation, not a framework

  • Some choices are done for the greater good
  • Not everything will work for you
  • You will spend a lot of time bending the implementation to your needs
  • This will cause some pain

Pain #1: Username and email as identifier

  • Login form uses username by default πŸ‘€
  • Both email and username are unique, so email is also valid as identifier β˜‘
  • Most professional websites use email as identifier
  • You can't really switch to emails only πŸ˜₯

Pain #1: Username and email as identifier

Two user provider to chose from:

  • fos_user.user_provider.username
  • fos_user.user_provider.username_email

Pain #1: Username and email as identifier

  • Symfony refers to username everywhere:
    • UserProviderInterface::loadUserByUsername
    • UserInterface::getUsername
  • It really mean "identifier", and may change in the future...
  • It's ok to have getUsername return an email
  • But removing the username field is not possible, you have to hack!

Pain #1: Bye Username

  • Hack the entity setter:
    public function setEmail($email) {
        $email = is_null($email) ? '' : $email;
        parent::setEmail($email);
        $this->setUsername($email);
        return $this;
    }
  • Remove the field from the FormType:
    public function buildForm(FormBuilder $builder, array $options) {
        parent::buildForm($builder, $options);
        $builder->remove('username');
    }

Pain #1: Bye Username

  • Completely rewrite the validation constraints and implement new validation groups
  • Switch the user provider to email OR username
  • Full instructions here
  • The username field is still in the database 😑

Pain #1: username_email provider is a hack ⚠

public function findUserByUsernameOrEmail($usernameOrEmail)
{
    if (preg_match('/^.+\@\S+\.\S+$/', $usernameOrEmail)) {
        return $this->findUserByEmail($usernameOrEmail);
    }

    return $this->findUserByUsername($usernameOrEmail);
}

Creative usernames will never be found:
[@lpha.org], πœ‹ @3.14...

Pain #2: Database

The default table is full of fields you may never use
and 🚫 cannot be removed 🚫

  • username and username_canonical
  • email_canonical ⚠
  • salt, not needed since bcrypt (UserInterface)
  • last_login, an UPDATE on each login ⚠
  • roles is (DC2Type:array), no interoperability

Pain #2: Database

  • AdvancedUserInterface is implemented πŸ™Œ
    • locked
    • expired
    • expires_at
    • credentials_expired
    • credentials_expire_at
  • Doctrine's AttributeOverrides may allow to edit some fields options, but no removing or type switching.

Pain #3: Wording

Obviously default template must be customized 🎨

Register FOSUserBundle

Pain #3: Wording

  • Setting your own layout:
    app/Resources/FOSUserBundle/views/layout.html.twig
  • Special block fos_user_content:
    {% extends "::base.html.twig" %}
    {% block body %}
        {% block fos_user_content %}{% endblock fos_user_content %}
        {{ parent() }}
    {% endblock body %}
  • You HAVE to display flash messages

Pain #3: Wording

FOSUserBundle register nice

Pain #3: Wording

The user has been created successfully

😐 πŸ€”

Pain #3: Wording

  • Good: All the texts are provided and translated in 40 languages 🌍 thanks to awesome contributors πŸ‘
  • Bad: Messages are very generics, so you still have some translation work to do ✍

Pain #3: Wording

app/Resources/translations/FOSUserBundle.en.yml

registration:
    flash:
        user_created: >
            Your account has been created successfully,
            congratulation!

Pain #3: Wording

  • Emails are Twig files with translations,
    and only the txt part is provided
  • The Mailer use Twig blocks to get the subject, text and html body parts
  • There is a MailerInterface, because the default one is opinionated
  • I often build a custom FOSUser Mailer as bridge to my real application Mailer πŸ‘

Pain #4: Extendability

  • Adding a field to the user profile is easy, it's your entity:
    /**
     * @var \DateTime
     * @ORM\Column(name="birth_date", type="date")
     * @Assert\LessThan("-13 years", groups={"Registration", "Profile"})
     */
    protected $birthDate;
  • DO NOT forget the validation groups if you need validation
  • What about form types then?

Pain #4: Extendability

class RegistrationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('birthDate');
    }

    // Extends the bundle form type
    public function getParent()
    {
        return RegistrationFormType::class;
    }
}

Pain #4: Extendability

fos_user:
    ...
    registration:
        form:
            type: "AppBundle\Form\RegistrationType"

To do for both the registration and profile types πŸ˜“

File madness 🚨

app/Resources
β”œβ”€β”€ FOSUserBundle
β”‚   └── views
β”‚       β”œβ”€β”€ Registration
β”‚       β”‚   β”œβ”€β”€ email.txt.twig
β”‚       β”‚   β”œβ”€β”€ confirmed.html.twig
β”‚       β”‚   β”œβ”€β”€ check_email.html.twig
β”‚       β”‚   └── register_content.html.twig
β”‚       β”œβ”€β”€ Resetting
β”‚       β”‚   β”œβ”€β”€ check_email.html.twig
β”‚       β”‚   β”œβ”€β”€ request.html.twig
β”‚       β”‚   └── reset.html.twig
β”‚       └── Security
β”‚           └── login.html.twig
└── translations
    β”œβ”€β”€ FOSUserBundle.en.yml
    └── validators.en.yml
src/
└── AppBundle
    β”œβ”€β”€ Form
    β”‚Β Β  β”œβ”€β”€ RegistrationType.php
    β”‚Β Β  └── ProfileType.php
    └── Manager
     Β Β  └── FOSUserEmail.php

So much files and overwriting

Very soon in your projects, you will note that
you are building a lot on top of the Bundle, increasing your dependency on it.

That's called Technical Debt.

Why not drop the bundle and write them anyway? πŸ€”

πŸ₯– Bonus pain πŸ₯–

Separated User entities

  • You need a Customer entity and an Admin entity?
  • Install PUGXMultiUserBundle (ORM only), a Bundle that extends FOSUserBundle!
  • 🚨 At your own risks 🚨

Separated Login Forms

  • Custom login form for backend and customer access?
    • custom routes for everything
    • a new firewall and new access_control entries
    • a new SecurityController extending the bundle one, with some conditions to know what template to render πŸ™ˆ
    • and of course the new template

Why you shouldn't use it

  • Technical debt generator if you need customization
  • Hard to adapt, lots of file to write
  • Hardly maintained 😧 (maybe not anymore)
  • Makes visible your usage of Symfony + FOSUserBundle, which can be a security concern πŸ•΅
inurl:"/resetting/request" "Username or email address"

Why we need it!

  • Symfony Security is hard to learn πŸ”
  • It enables a WOW effect for new developers πŸ˜ƒ
  • Still a great piece of software if you stay in the lines 😍

Why we need it!

  • This is the most used third party bundle πŸ“ˆ
  • It's the entry point of a lot of new users,
    the user base that does not know Symfony very well πŸ‘Ά

Maybe should help do better for the sake of Symfony? FOSUserBundle should be helpful, not scary.

Hardly maintained 😰

Issue overflow: everyone asks questions about the Symfony Security here, thinking Security is FOSUser concern:

Issue 2290

Hardly maintained 😰

  • Plenty of other examples, just looking at the first page:
    • #2251 is resolved but the user does not close it
    • #2223 is out of topic
    • #2206 is user pushing stress on contributors to release a new version
    • #2215 is question about email going to spam...

Hardly maintained 😰

  • No release for more than a year (until this month)
  • Slowed down by it's own popularity πŸ™‰
  • Also, no strong labels management, no triage like FOSRest or FOSHttpCache πŸ—ƒ

πŸ“† Hack Day πŸ“†

  • What are you doing this Saturday?
  • Let's try to help the most used Bundle. Let's try to help newcomers.
  • That's some serious "DX" (Developer eXperience) task!
  • I want to help!

Building your own πŸ’ͺ

Building your own

  • Your own entities, no fuss, no extra fields
  • Your own mailer from the start
  • 100% flexibility, 100% integrated to your code base
  • Some code to write:
    • Your forms for register / profile and password reset
    • The appropriate controllers
    • The User Manager itself to create / persist, send emails, etc.

The User Entity

There is a cookbook for that

  • Create your User entity, implement UserInterface
  • Configure Security to load from your Entity,
    that's part of Symfony

The Registration form

There is a cookbook for that

  • Create a Form for the Entity
  • Handle the Form Submission in a custom controller, where you have to encode the password
  • Write a template

Building your own

That's basic Symfony development. Same for password reset, profile edit...

Just some decisions to make based on your needs and environment πŸ‘‡ πŸ‘ˆ πŸ‘‰
Like, do you need canonical fields?

Canonical fields

  • FOSUserBundle adds canonical versions of all the username and email fields
  • mb_convert_case($string, MB_CASE_LOWER);
  • This makes sure we can't have two accounts with the same email:

foobar@example.org
FoOBAR@example.org

Canonical fields

  • Do you know what an SQL collation is for?
  • … COLLATE utf8mb4_unicode_ci ENGINE = InnoDB;
  • It tells how to sort the data in your table, to make sure "A" and "a" are before "Z" and "z"... which may not be the case in binary sort order

With a proper collation

Γ’ = Γ„ = A = a

🀘

Canonical fields

It works for SELECT:

SELECT 'fΓΆoBAR@example.org' COLLATE UTF8_GENERAL_CI
    = 'foobar@example.org' COLLATE UTF8_GENERAL_CI;


=> 1

Canonical fields

It also works with UNIQUE KEY:


CREATE TABLE `test` (
  `username` varchar(180) COLLATE utf8mb4_unicode_ci,
  UNIQUE KEY `uniqueindextest` (`username`)
) ENGINE=InnoDB DEFAULT
CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;


INSERT INTO `test` (username) VALUES
    ('FOOBAR@example.org'), ('foobΓ€r@example.org');


=> Duplicate entry 'foobΓ€r@example.org'
=> for key 'uniqueindextest'

You do not need Canonical fields

πŸ‘‹

Choosing a bundle

The right way Β©

Where to look

  • http://symfohub.com/ is dead 😭
  • KNPBundles.com is ok but not up to date 😐
  • Packagist.org is the best source! 😍
    • Add &type=symfony-bundle to filter only the Symfony Bundle!
Before
After

Do not use Composer to install
πŸ˜… jQuery πŸ˜…

Please

What to check

  • Is it maintained? Can I contribute? Bus factor?
  • Is there any tests? Are they green?
  • Is there a recent stable version?
  • Is there a reasonable amount of issues?
  • What are the dependencies? What are the dependencies of the dependencies?!

Almost 8200 bundles to choose from

  • Never install a bundle you do not trust 100%
  • What does it do and how easily can it be changed?
  • Is there a complete documentation?
  • Look at the issue tracker for long pending issues
  • Does it add a real feature to Symfony, or is it just a library?

Wrapper Bundles

  • Wrapper around a perfectly fine PHP library
  • The library moves faster than the bundle, and you have to update things manually
  • In some cases the Symfony Bundle adds constraints on top of the Library, like FOSElasticaBundle:
    • Adds strict Configuration for the Elasticsearch mapping
    • Elasticsearch mapping move faster than the bundle
    • This has nothing to do with Elastica BTW

Implementation Bundles

  • FOSUserBundle is an User Management implementation
  • You can use it if it fits your needs well
  • Otherwise you add a burden on your project, in term of maintenance and quality
  • You do not want a 5000+ lines of code Bundle to only use some parts of it

3rd party Bundles

  • Keep your number of Bundle as low as possible ⬇
  • Prefer libraries over Bundles πŸ’Ž πŸ’
  • Stay awesome πŸ’—

🌯 Wrap up 🌯

  • FOSUserBundle is a great tool, but it's an opinionated implementation, and should not be used on professional applications:
    • Too much code to write on top of it for customization
    • Makes maintenance and updates harder
  • We can help, join on Saturday πŸ‘

🌯 Wrap up 🌯

  • Building your own User Management system is easy
  • Third party bundles are good for quick win, but think about the long term consequences
  • Each Bundle you install is long term engagement 🚧

Thanks to all FOSUserBundle contributors

I'm done ✌

Throw your questions at me!

❀ Did I told you I love emoji? πŸ¦„

Me
@damienalexandre

Personnal Wishlist!

  • Add strong label management for issues
  • Remove the Groups
  • Remove the AdvancedUserInterface, but provide example of how to add it
  • Remove the username field, but provide example of how to use it
  • Remove the last_login field, or provide a way to disable the listener
  • https://github.com/FriendsOfSymfony/FOSUserBundle/issues/1309