Buy

The UserInterface Methods (Keep some Blank!)

Our job: fill in the 5 methods from UserInterface. But check this out - I'm going rogue - I'm only going to implement 2 of them: getUsername() and getRoles().

getUsername()

First getUsername()... is super-unimportant. Just return any unique user string you want - a username, an email, a uuid, a funny, but unique joke - whatever. This is only used to show you who is logged in when you're debugging.

In our app, our users won't have a username - but they will have an email. Add a private $email property then just use that in getUsername(): return $this->email:

48 lines src/AppBundle/Entity/User.php
... lines 1 - 7
class User implements UserInterface
{
private $email;
// needed by the security system
public function getUsername()
{
return $this->email;
}
... lines 17 - 46
}

And add a setter for that method:

48 lines src/AppBundle/Entity/User.php
... lines 1 - 7
class User implements UserInterface
{
... lines 10 - 42
public function setEmail($email)
{
$this->email = $email;
}
}

We'll eventually need that to create users.

getRoles()

Cool: half-way done! Now: getRoles(). We'll talk roles later with authorization, but these are basically permissions we want to give the user. For now, give every user the same, one role: ROLE_USER:

42 lines src/AppBundle/Entity/User.php
... lines 1 - 7
class User implements UserInterface
{
... lines 10 - 16
public function getRoles()
{
return ['ROLE_USER'];
}
... lines 21 - 40
}

What about getPassword(), getSalt() and eraseCredentials()?

So what about getPassword(), getSalt() and eraseCredentials()? Keep them blank:

42 lines src/AppBundle/Entity/User.php
... lines 1 - 7
class User implements UserInterface
{
... lines 10 - 21
public function getPassword()
{
// leaving blank - I don't need/have a password!
}
public function getSalt()
{
// leaving blank - I don't need/have a password!
}
public function eraseCredentials()
{
// leaving blank - I don't need/have a password!
}
... lines 36 - 40
}

Whaat? It turns out, you only need these if your app is personally responsible for storing and checking user passwords. In our app - to start - we're not going to have passwords: we're just going to let anyone login with a single, central, hardcoded password. But there are also real-world situations where your app isn't responsible for managing and checking passwords.

If you have one of these, feel ok leaving these blank.

Setting up the User Entity

So in our application, we want to store users in the database. So let's set this class up with Doctrine. Copy the use statement from SubFamily and paste it here:

63 lines src/AppBundle/Entity/User.php
... lines 1 - 6
use Doctrine\ORM\Mapping as ORM;
... lines 8 - 63

Next, I'll put my cursor inside the class, press Command+N - or use the "Code"->"Generate" menu - and select "ORM class":

63 lines src/AppBundle/Entity/User.php
... lines 1 - 8
/**
* @ORM\Entity
* @ORM\Table(name="user")
*/
class User implements UserInterface
... lines 14 - 63

Next, add a private $id property and press Command+N one more time. This time, choose "ORM annotation" and highlight both fields:

63 lines src/AppBundle/Entity/User.php
... lines 1 - 12
class User implements UserInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", unique=true)
*/
private $email;
... lines 26 - 61
}

Oh, and while we're here, let's add unique=true for the email field:

63 lines src/AppBundle/Entity/User.php
... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 21
/**
* @ORM\Column(type="string", unique=true)
*/
private $email;
... lines 26 - 61
}

Perfect: we have a fully functional User class. Sure, it only has an id and email, but that's enough!

Since we just added a new entity, let's generate a migration:

./bin/console doctrine:migrations:diff

I'll copy the class and open it up quickly, just to make sure it looks right:

35 lines app/DoctrineMigrations/Version20160524105319.php
... lines 1 - 10
class Version20160524105319 extends AbstractMigration
{
... lines 13 - 15
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
}
... lines 23 - 26
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE user');
}
}

Looks great!

Adding User Fixtures

Finally, let's add some users to the database. Open our trusty fixtures.yml. I'll copy the SubFamily section, change the class to User and give these keys user_1..10:

26 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
... lines 1 - 22
AppBundle\Entity\User:
user_{1..10}:
... lines 25 - 26

Then, the only field is email. Set it to weaverryan+<current()>@gmail.com:

26 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
... lines 1 - 22
AppBundle\Entity\User:
user_{1..10}:
email: weaverryan+<current()>@gmail.com

The current() function will return 1 through 10 as Alice loops through our set. That'll give us weaverryan+1@gmail.com up to weaverryan+10@gmail.com. And if you didn't know, Gmail ignores everything after a + sign, so these will all be delivered to weaverryan@gmail.com. There's your Internet hack for the day.

Ok, let's roll! Don't forget to run the migration first:

./bin/console doctrine:migrations:migrate

And then load the fixtures:

./bin/console doctrine:fixtures:load

Hey hey: you've got users in the database. Let's let 'em login.

Leave a comment!

  • 2017-03-09 somecallmetim27

    Awesome! Worked like a charm. My sincerest thanks :)

  • 2017-03-09 Victor Bocharsky

    Hey somecallmetim27 ,

    Good detective work! How do you encode your password and where? I suppose you do it in the event listener where encode plain password and store the encoded value in User::$password (as we do in our screencasts). Then in this event listener you have to add one more extra check: if user's plain password is null - then skip encoding it, because user doesn't set a new password.

    Cheers!

  • 2017-03-09 somecallmetim27

    I'm working on a separate project, but I've been using the security stuff I've learned here in my projects since. I am, however, currently running into a problem. I want to be able to promote (add ROLE_ADMIN) and demote (remove ROLE_ADMIN). This in and of itself isn't difficult;


    /**
    * @Route("/demote_user/{id}", name="demote_user")
    */
    public function demoteUserAction($id){
    $em = $this->getDoctrine()->getManager();
    $user = $em->getRepository('AppBundle:User')->find($id);

    //with corresponding method in the user entity
    $user->removeOneRole('ROLE_ADMIN');

    $em->persist($user);
    $em->flush();

    return $this->redirectToRoute('list_users');
    }

    The trouble is that the user password is getting overwritten for reasons I don't entirely understand. Though you can see from dump;die


    1 => User {#527 ▼
    -id: 8
    -email: "test@gmail.com"
    -password: "$2y$13$oy2nRiXznzp1I9jrfHRSR.OGtvgR38S.sO3pdaoWWFP1WZhUlMeFq"
    -plainPassword: null
    -roles: array:1 [▶]
    }

    that plainPassword is getting set to null and therefore getting pushed onto the db overwriting the existing password. I know the problem, just not how to fix it...

  • 2017-01-07 weaverryan

    Woohoo! Nice job finding the problem! The toughest bugs are when it's some small, tiny detail like this :D. Now, keep going!!

  • 2017-01-07 ehymel

    OK, I found the problem. Your hints helped figure it out. I somehow had a src/AppBundle/ORM/fixtures.yml file, but that is the wrong directory. I corrected it to src/AppBundle/DataFixtures/ORM/fixtures.yml and all is working now!!

    Thanks very much.

  • 2017-01-07 ehymel

    That error does not show.

    I did confirm that if I manually load data into the users table (i.e., add a few email addresses), those entries do get wiped. That may be irrelevant depending on how the db gets purged. I don't see anything else in the User class that would affect this. I tried to paste the src/AppBundle/Entity/User.php file here but the comments in the code mess up the post here.

  • 2017-01-07 weaverryan

    Hey ehymel!

    Wow, that IS weird... we definitely know the fixtures are working... but strange that *only* the User table is *not* being loaded! Let's do some detective work: find the setEmail method in User and add this code:


    public function setEmail($email)
    {
    throw new \Exception('setEmail called with '.$email);

    $this->email = $email;
    }

    When you load your fixtures, do you now see this error? And does it show the correct email address (i.e. weaverryan+1@gmail.com)? I want to make sure that (1) the User fixtures are being loaded and (2) that there is not some *other* fixtures code for the User class somewhere else in the fixtures file that might be overriding this code.

    Let me know what you find out! I've definitely never seen this problem before - so I bet it's something minor!

    Cheers!

  • 2017-01-07 ehymel

    I added these lines to my src/AppBundle/ORM/fixtures.yml script (from previous lessons)

    AppBundle\Entity\User:
    user_{1..10}:
    email: weaverryan+<current()>@gmail.com

    and then from the command line I run:

    ./bin/console doctrine:fixtures:load

    It purges the database and loads in new data as expected, However, my user table is empty. All other tables have (new) data. Any hints why this would not be working?

  • 2016-11-29 weaverryan

    Lampje Hmm, yea, I agree - it all looks good to me. If you use <email()> instead, do you get the same error? I mean, duplicate constraint "weaverryan+1@gmail.com"? Or is a duplicate constraint error with a *different* email (I'm trying to see if weaverryan+1@gmail.com is sneaking into the code in some other way). Or, what happens if you change weaverryan+ to something else in the fixtures file? Basically, if you play around a bit, do you see any other strange behavior?

    It was a good idea to copy the code from the finished dir: those files are created programmatically from the *real* code that worked for the screencast, so it's definitely correct/good files to work from.

    Let me know if you find out anything else - this is *very* strange!

  • 2016-11-29 Lampje

    Yes, i get `'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'weaverryan+1@gmail.com' for key

    'UNIQ_8D93D649E7927C75''` 3 times.

    I'm a bit further in the chapter and this still persists.. I've copied the .yml from the 'finished' dir, just to be sure that this doesn't have to do with tab's or something silly like that.

    I've checked A, no other entries than:


    AppBundle\Entity\Genus:
    AppBundle\Entity\GenusNote:
    AppBundle\Entity\SubFamily:
    AppBundle\Entity\User:

    No luck on B neither:


    AppBundle\Entity\User:
    user_{1..10}:
    email: weaverryan+<current()>@gmail.com

    C:


    /**
    * @param string $email
    */
    public function setEmail( $email ) {
    $this->email = $email;
    }

    Don't know if it matters, i'm using: 10.1.9-MariaDB as database.

  • 2016-11-29 weaverryan

    Hey Lampje!

    Bah, that is strange! When you use <email()> (good idea btw), what exact error do you get? Does it say "Duplicate entry 'weaverryan+1@gmail.com'"? Or does it say "Duplicate entry" but with a different email address? I'm sure this is something small, I would check a few things:

    A) Make sure you don't have any other AppBundle\Entity\User entries in your fixtures file somewhere
    B) Make sure don't have another email key under your users in the fixtures file
    C) Double-check that your setEmail() method in User doesn't have anything funny

    I've never had a problem with the randomization with Alice... which is why I'm looking for a simple answer :).

    Cheers!

  • 2016-11-29 Lampje

    On the bottom of page 2, i'm trying to load the fixtures but it returns this error:

    [Doctrine\DBAL\Exception\UniqueConstraintViolationException]
    An exception occurred while executing 'INSERT INTO user (email) VALUES (?)' with params ["weaverryan+1@gmail.com"]:
    SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'weaverryan+1@gmail.com' for key 'UNIQ_8D93D6
    49E7927C74'

    [Doctrine\DBAL\Driver\PDOException]
    SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'weaverryan+1@gmail.com' for key 'UNIQ_8D93D6
    49E7927C74'

    [PDOException]
    SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'weaverryan+1@gmail.com' for key 'UNIQ_8D93D6
    49E7927C74'

    The user table remains empty...
    Changing fixtures.yml to email: <email()> gives the same result (3 errors)

    Manually inserting directly into the table causes no problems.
    Que pasa?

  • 2016-11-25 zied haj salah

    Hi Ryan, very clear now , it works for me.
    I knew migrations from rails and when I got stack I thought that it is not the same idea, but your answer clarified everything.
    Thanks.

  • 2016-11-24 weaverryan

    Hey Zied!

    Ah, here is the secret! In your database, the migrations library creates and manages a table called migration_versions. Whenever you execute a migration, it adds a new row in this table to record that the migration has finished. In fact, each time that you run doctrine:migrations:migrate, it looks at each file in your app/DoctrineMigrations directory, and checks the migration_versions table for each file to see if it has already been executed. If it *has*, it does not execute it. If it has *not*, then it executes it and records a new row in the table.

    In our project (and this is the way I recommend doing it), we have created a migration for *every* database change (i.e. a migration to create every table). My guess is that you may have somehow created the genus table via some method *other* than running your migrations (e.g. by running doctrine:schema:update --force or doctrine:schema:create). Basically, you should never do this :), because it means that you have a genus table, but your migration_versions table is still empty. So when you try to execute your migrations, it runs *all* of the migrations, including those that try to build the genus table.

    This whole system that uses the migration_versions table is designed so that things are very predictable when you deploy to production: when you run doctrine:migrations:migrate, Doctrine will look at the migration_versions table in your production database to determine exactly which migration files have already been executed and which have not been executed yet.

    If you find yourself in a spot where your system is "messed up" and you've accidentally created some tables in a method *other* than by running your migrations, then I recommend (locally on your development machine) to restart from scratch:


    bin/console doctrine:database:drop --force
    bin/console doctrine:database:create
    bin/console doctrine:migrations:migrate

    This will drop the database, which is important because it will drop your migration_versions table. Then, it will run all of your existing migrations. NOW, you can create a new entity and run doctrine:migrations:diff. It will correctly generate, and when you run doctrine:migrations:migrate, Doctrine will know that you only have *one* new migration to execute.

    Let me know if this helps! Cheers!

  • 2016-11-24 zied haj salah

    Thank for your explanation.
    1- What i doesn't understand is the following:
    When i create a new entity and then i run doctrine:migrations:diff, it will generate a migration to create only the table corresponding to this entity. So why when i run doctrine:migrations:migrate it attempt to execute the older migrations not only the latest.
    2- I figure out the situation by running doctrine:migrations:execute 20161124132920 (the last migration) is that correct?

  • 2016-11-24 Victor Bocharsky

    Hi Zied,

    First of all, *you* should always double check the queries inside any new migration, you can't just rely on Doctrine here. So if you see something that could break your production, i.e. potential data loss - then you need to correct the migration queries.

    At second - you should understand well how migrations works and how to use it. If you decide to manage your DB with migrations - then you better need to forget about "bin/console doctrine:schema:update --force", because if you tweak your DB schema with this command - I bet your migrations failed on the next migrate.

    Now let's consider the example: you create a new entity. It's a new entity, so it's a new table, which you have not had on prod yet, i.e. in your DB on prod one missing table for now. When you run "bin/console doctrine:migration:diff" - it will create a migration with query which create this table. Then you will need to run this migration on production server which creates this new table for you. So in this case no any data loss, everything is fine.

    P.S. you probably won't lose your data on prod when migration recreate a table - this migration just will fail with error "Table already exist".

    I hope I understand you right and it'll clarify something for you. Let me know if it makes sense for you.

    Cheers!

  • 2016-11-24 zied haj salah

    Hello Victor
    I have the same case.
    here i don't understand why Migration have to create genus table that already exists and the last migration file generated via doctrine:migrations:diff don't include any database action related to this table. I mean if i was in production i risque to loose my data to recreate this table?

  • 2016-09-12 weaverryan

    Hey Yang!

    Yes, you definitely understand things correctly :). But, doctrine:fixtures:load is pretty smart: it attempts to *order* the DELETE queries to avoid constraint violations (e.g it would delete the Genus table before trying to delete the SubFamily table). This doesn't *always* work (sometimes you have circular references), but it does *most* of the time. But you're also right that if you set the "on delete" behavior, then that would make deleting easier. For example:


    // in Genus.php
    /**
    * @ORM\ManyToOne(targetEntity="AppBundle\Entity\SubFamily")
    * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
    */
    private $subFamily;

    With this setup, if a SubFamily were deleted, then all related Genus would also be deleted. As you know, this is done by setting the foreign key relationship in the database (i.e. the delete is done by the database itself). I usually *don't* set onDelete behavior until/unless I need it... because bad things can happen :).

    Cheers!

  • 2016-09-12 Yang Liu

    hm... in the script, you used bin/console doctrine:fixtures:load to update the fixtures. But I thought it won't work, at least not directly? As I understand it, fixtures:load always try to purge the old datas from the tables and then create new. I got some constraint violation with subfamily and genus entities. I guess its because the subFamily in the genus entity is not set to "discard on delete/update"? So I had to drop the database and recreate this, then run migrate and fixutres:load. This works then.

  • 2016-08-25 weaverryan

    Hey Zoé!

    Hmm, yea - some of the code blocks on this page were out-of-date! Thanks for leaving this comment! In the video, we *only* create an email field (not a username field) and return the email field in getUsername(). I just updated the incorrect code blocks to reflect this. But of course, you can do whatever you want - and your setup is equally valid :).

    Cheers!

  • 2016-08-25 Zoé Belleton

    In the class \AppBundle\Entity\User, the property is $username, not $name. So I have updated my fixtures.yml to match with this :

    AppBundle\Entity\User:
    user_{1..10}:
    username: <lastname()>
    email: weaverryan+<current()>@gmail.com
    The opposite operation is possible too : change the property to $name and there will be no need to update the fixtures.

  • 2016-08-25 Victor Bocharsky

    Ah, usually database creation is up to you. Migrations could create tables as you can see in the code of migrations for this course, but you have to create a DB manually, i.e. I mean you could create it easily by running the command:


    $ bin/console doctrine:database:create

    And when your DB exists - then start running migrations!

    Cheers!

  • 2016-08-25 claire

    Hey Victor,

    aaa I see thank you for explaining that. I dropped the table and I ran migration but now getting this error:

    [Doctrine\DBAL\Exception\ConnectionException]

    An exception occured in driver: SQLSTATE[HY000] [1049] Unknown database 'claire_test'

    I used ./bin/console doctrine:migrations:migrate

    Is that the command to use to run all migrations ?

    Thank you!

  • 2016-08-25 Victor Bocharsky

    Hey @claire,

    You did it right to drop this table, but you didn't have to create it. Migration will create this table for you. This's exactly what migration trying to do based on the error message you got: "Base table or view already exists: 1050 Table 'genus' already exists". So the right solution is: Drop `genus` table and don't create it, just run a migration. If you will still get similar errors, try to drop database at all and run all migrations from scratch. And don't forget to load fixtures after it.

    Cheers!

  • 2016-08-25 claire

    Hi,

    I having a bit of trouble with the migration. In the first instance I got an error that said the tables already exisit etc. So I dropped them and created them again and then did the migration. Im still getting the same error.

    Migration 20160818095005 failed during Execution. Error An exception occurred while executing 'CREATE TABLE genus (id INT AUTO_INCREMENT NOT NULL, sub_family_id INT NOT NULL, name VARCHAR(255) NOT NULL, species_count INT NOT NULL, fun_fact VARCHAR(255) DEFAULT NULL, is_published TINYINT(1) NOT NULL, first_discovered_at DATE NOT NULL, INDEX IDX_38C5106ED15310D4 (sub_family_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB':

    SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'genus' already exists.

    The database was working before adding the User Table and I am not too sure where I have gone wrong here.

    Thank you,
    Claire