Buy

Saving Users

Now that the error is gone, try logging in! Wait, but our user table is empty. So we can see the bad password message, but we can’t actually log in yet.

But we’re pros, so it’s no problem. Let’s copy the LoadEvents fixtures class (LoadEvents.php) into the UserBundle, rename it to LoadUsers, and update the namespaces:

// src/Yoda/UserBundle/DataFixtures/ORM/LoadUsers.php
namespace Yoda\UserBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Yoda\UserBundle\Entity\User;

class LoadUsers implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        // todo
    }
}

Saving users is almost easy: just create the object, give it a username and then persist and flush it.

The tricky part is that darn password field, which needs to be encoded with bcrypt:

// src/Yoda/UserBundle/DataFixtures/ORM/LoadUsers.php
// ...
use Yoda\UserBundle\Entity\User;
// ...

public function load(ObjectManager $manager)
{
    $user = new User();
    $user->setUsername('darth');
    // todo - fill in this encoded password... ya know... somehow...
    $user->setPassword('');
    $manager->persist($user);

    // the queries aren't done until now
    $manager->flush();
}

ContainerAwareInterface for Fixtures

But what’s cool is that Symfony gives us an object that can do all that encoding for us. To get it, first make the fixture implement the ContainerAwareInterface:

// src/Yoda/UserBundle/DataFixtures/ORM/LoadUsers.php
// ...

use Symfony\Component\DependencyInjection\ContainerAwareInterface;

class LoadUsers implements FixtureInterface, ContainerAwareInterface
{
    // ...
}

This requires one new method - setContainer. In it, we’ll store the $container variable onto a new $container property:

// src/Yoda/UserBundle/DataFixtures/ORM/LoadUsers.php
// ...

use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class LoadUsers implements FixtureInterface, ContainerAwareInterface
{
    private $container;

    // ...

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }
}

Because we implement this interface, Symfony calls this method and passes us the container object before calling load. Remember that the container is the array-like object that holds all the useful objects in the system. We can see a list of those object by running the container:debug console task:

php app/console container:debug

Encoding the Password

Let’s create a helper function called encodePassword to you know encode the password! This step may look strange, but stay with me. First, we ask Symfony for a special “encoder” object that knows how to encrypt our passwords. Remember the bcrypt config we put in security.yml? Yep, this object will use that.

After we grab the encoder, we just call encodePassword(), grab a sandwich and let it do all the work:

// src/Yoda/UserBundle/DataFixtures/ORM/LoadUsers.php
// ...

private function encodePassword(User $user, $plainPassword)
{
    $encoder = $this->container->get('security.encoder_factory')
        ->getEncoder($user)
    ;

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

Behind the scenes, it takes the plain-text password, generates a random salt, then encrypts the whole thing using bcrypt. Ok, so let’s set this onto the password property:

// src/Yoda/UserBundle/DataFixtures/ORM/LoadUsers.php
// ...

public function load(ObjectManager $manager)
{
    $user = new User();
    $user->setUsername('darth');
    $user->setPassword($this->encodePassword($user, 'darthpass'));
    $manager->persist($user);

    // the queries aren't done until now
    $manager->flush();
}

Try it! Reload the fixtures from the command line:

php app/console doctrine:fixtures:load

Let’s use the query console task to look at what the user looks like:

php app/console doctrine:query:sql "SELECT * FROM yoda_user"
array (size=1)
  0 =>
    array (size=3)
      'id' => string '1' (length=1)
      'username' => string 'user' (length=4)
      'password' => string '$2y$13$BoVE3I5dmVkBjRp.l6uwyOI8Z8Ngokiaa.OUUuHoDbGDBdMRMUrmC' (length=60)

Nice! We can see the encoded password, which for bcrypt, also includes the randomly-generated salt. You do need to store the salt for each user, but with bcrypt, it happens automatically. Symfony requires us to have a getSalt function on our User, but it’s totally not needed with bcrypt.

Back at the browser, we can login! Behind the scenes, here’s basically what’s happening:

  1. A User entity is loaded from the database for the given username;
  2. The plain-text password we entered is encoded with bcrypt;
  3. The encoded version of the submitted password is compared with the saved password field. If they match, then you now have access to roam about this fully armed and operational battle station!

Leave a comment!

  • 2016-10-14 Victor Bocharsky

    You could easily check permissions with something like {% if app.user.id == post.user.id %} - then show links, but let's use something cooler! Look at Symfony Voters - it's exactly what you need. then you can check whether the current user has access to a post with is_granted() Twig function. And hey, we have a free course on it: Symfony Security Voters. And one more useful screencast about voters with a nice example inside: https://knpuniversity.com/scre... .

    Cheers!

  • 2016-10-14 Dan Costinel

    I'll give it a try. Thanks a lot!
    And one more question if you don't mind to answer it.

    I have, for this part of my template, the need to hide some links (edit and delete) if the current logged in user is not the owner of the respective post (I have a ManyToOne relationship)

    Like:

    Users
    -------
    id username
    1 abc
    2 def

    Posts
    -------
    id body user_id
    1 ... 1
    2 ... 1
    3 ... 2

    How to achieve this?

  • 2016-10-14 Victor Bocharsky

    It's simple enough - just create a separate form for each post row as you do with links ;)


    {% for post in posts %}
    <form action="{{ path('post_delete', {id: post.id}) }}" method="DELETE">
    <input type="submit">
    </form>
    {% endfor %}

    i.e. use its own separate form for each product row.

    Cheers!

  • 2016-10-14 Dan Costinel

    Hi Victor, thanks for replying!

    I tried your second solution too, but I couldn't figure out how to add a different id for each delete_form form.

    Tried something like:


    // loop through posts
    // ...
    Delete
    // ...
    // endloop

    And I used both deleteAction(Request $request, $id) and createDeleteForm($id) generated by CRUD.
    I ended up with just one delete button, and I needed one for each post.

  • 2016-10-14 Victor Bocharsky

    Hey Dan,

    Actually, you did it right, but there's more robust solution with a form which sends with DELETE method.

    The "@Method("DELETE")" annotation means that you can send only DELETE request to this route, i.e. you can't send DELETE request with a link because it's just a GET request. So there're a few solutions:

    1. Change "@Method("DELETE")" to the @Method("GET") because you delete posts by clicking a link. - Like you did
    2. Replace delete link in the template with a <form method="DELETE"></form>, because you can send DELETE requests only with form. Of course, you need to add a submit button to this form and also generate the action attribute which points to the deleteAction().

    The 2nd solution is more robust, because with the 1st one you can accidently go directly to the delete action and remove a post which you don't want to delete.

    Cheers!

  • 2016-10-14 Dan Costinel

    Damn, I'm such a noob!

    I finally solve the problem, by replacing the "DELETE" method with "GET". And boom, it worked like a charm!

    I hope someone will find this helpful! ... and I'm glad I've "enlighted" myself!

    But high-five KNP, just in case! :)

  • 2016-10-14 Dan Costinel

    Hey KNP! I'm sorry for bothering you again!

    So I need to create a custom deleteAction() method. My reliable friend, google, didn't cared about my problem, unfortunately. Muf! Muf!

    Anyway, here's what I've wrote until now:


    /**
    * Deletes a Post entity.
    *
    * @Route("/post/{id}", name="post_delete")
    * @Method("DELETE")
    */
    public function deleteAction(Request $request, $id)
    {
    $em = $this->getDoctrine()->getManager();
    $entity = $em->getRepository('AppBundle:Post')->find($id);

    if (!$entity) {
    throw $this->createNotFoundException('Unable to find Post entity.');
    }

    $em->remove($entity);
    $em->flush();

    return $this->redirect($this->generateUrl('dashboard'));
    }

    And in my twig template:


    // for post in posts
    // ...
    Delete
    // ...
    // endfor

    But I'm getting the following error:


    No route found for "GET /post/13": Method Not Allowed (Allow: DELETE)

    I also copied and adapted to my case, the code from a generated CRUD. It didn't worked!

    Help, I'm in panic!

  • 2016-05-13 BondashMaster

    and I have ALL THE POWERRRRRR!!! I got my suscription :D. This is gonna be a hell of a good weekend.

  • 2016-05-13 weaverryan

    Of course :) - I always keep an eye on the comments!

  • 2016-05-13 BondashMaster

    Thanks.

  • 2016-05-13 weaverryan

    You'll get access to *everything* while your subscription is active - including this and episode 1... and also all of the newer Symfony 3 tutorials if you want those :)

  • 2016-05-13 BondashMaster

    Little Question, If I get the $25 monthly acces. I get "Access to the full KnpUniversity course library". So that includes this tutorial in particular??? Or the current month tutorials??? Cause I want THIS course in particular. And also the episode 1(cause I check thats includes the datafixture part) But I'm not sure if I get them with the monthly acces.

  • 2016-05-13 weaverryan

    Hey there!

    Yea, I realize that's a little vague - sorry about that :). In this step, I copy the entire DataFixtures directory from EventBundle (which contains the LoadEvents.php file we created earlier) and copy it into UserBundle. I just did this to save me time from creating the DataFixtures/ORM directory in UserBundle and creating the new LoadUsers.php file by hand.

    I hope that helps!

  • 2016-05-12 BondashMaster

    I'm following this tutorial. But I get lost in "Let’s copy the LoadEvents fixtures class (LoadEvents.php)".