7

I've added an avatar image to my User class. When I wanted to render my edit form, I got this error

Serialization of 'Symfony\Component\HttpFoundation\File\File' is not allowed

I tried to solve the problem by implementing \Serializable in my User class according to Symfony Official Documentation. But when I implemented that,It redirected to login page and the Authentication turned to anon. and by logging in again, it redirected to login page again and stay anon. too.

I should mention that I have set some Authorizations. It will redirect you to the log in page if you are "anon." and want to access some protected routes.

Here is my UserEntity, User.php:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;


/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @ORM\Table(name="user")
 * @UniqueEntity(fields={"username"}, message="This username has been taken!")
 */
class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", unique=true,length=191)
     * @Assert\NotBlank()
     * @Assert\Length(min="5", minMessage="Username most contain at least 5 characters!")
     */
    private $username;

    /**
     * @ORM\Column(type="string")
     */
    private $password;

    /**
     * @ORM\Column(type="string")
     */
    private $displayName;

    /**
     * @ORM\Column(type="boolean")
     */
    private $showAdminBar;

    /**
     * @ORM\OneToMany(targetEntity="Post", mappedBy="owner")
     */
    private $posts;

    /**
     * @ORM\Column(type="string")
     */
    private $avatar;

    /**
     * @Assert\NotBlank(groups={"Registration"})
     * @Assert\Length(min="6", minMessage="Password most contain at least 6 characters!")
     */
    private $plainPassword;

    public function getUsername()
    {
        return $this->username;
    }

    public function getRoles()
    {
        return ['ROLE_ADMIN'];
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getSalt()
    {
    }

    public function eraseCredentials()
    {
        $this->plainPassword = null;
    }

    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->displayName,
            $this->avatar,
            // see section on salt below
            // $this->salt,
        ));
    }

    /**
     * @param mixed $username
     */
    public function setUsername($username)
    {
        $this->username = $username;
    }

    /**
     * @param mixed $password
     */
    public function setPassword($password)
    {
        $this->password = $password;
    }

    /**
     * @return mixed
     */
    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    /**
     * @param mixed $plainPassword
     */
    public function setPlainPassword($plainPassword)
    {
        $this->plainPassword = $plainPassword;
        //To make sure that Doctrine see the entity as "dirty"
        $this->password = null;
    }

    /**
     * @return mixed
     */
    public function getDisplayName()
    {
        return $this->displayName;
    }

    /**
     * @param mixed $displayName
     */
    public function setDisplayName($displayName)
    {
        $this->displayName = $displayName;
    }

    /**
     * @return mixed
     */
    public function getShowAdminBar()
    {
        return $this->showAdminBar;
    }

    /**
     * @param mixed $showAdminBar
     */
    public function setShowAdminBar($showAdminBar)
    {
        $this->showAdminBar = $showAdminBar;
    }

    /**
     * @return mixed
     */
    public function getPosts()
    {
        return $this->posts;
    }

    /**
     * @param mixed $posts
     */
    public function setPosts($posts)
    {
        $this->posts = $posts;
    }

    /**
     * @return mixed
     */
    public function getAvatar()
    {
        return $this->avatar;
    }

    /**
     * @param mixed $avatar
     */
    public function setAvatar($avatar)
    {
        $this->avatar = $avatar;
    }

    /**
     * @param mixed $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }
}

Here is my UserController.php

<?php

namespace App\Controller\Admin;

use App\Constants;
use App\Entity\User;
use App\Form\UserType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;


/**
 * @Route("/admin/user")
 * @Security("is_granted('ROLE_ADMIN')")
 */
class UserController extends Controller
{
    /**
     * @Route("/profile", name="admin_user_profile")
     */
    public function profileAction(Request $request)
    {
        $user = $this->getUser();

        $user->setAvatar(
            new File(Constants::UPLOAD_AVATAR.'/'.$user->getAvatar())
        );


        $form = $this->createForm(UserType::class, $user);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $user = $form->getData();


            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();

            $this->addFlash('success', 'Your Info Has Been Updated!');

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


        return $this->render('admin/user/profile.html.twig', [
            'user' => $user,
            'form' => $form->createView()
        ]);
    }

    /**
     * @Route("/list", name="admin_user_list")
     */
    public function listAction(Request $request)
    {
        $em = $this->getDoctrine()->getManager();
        $users = $em->getRepository(User::class)
            ->findAll();

        return $this->renderView('admin/user/list.html,twig',[
            'users' => $users
        ]);
    }
}

Here is my UserForm, UserType.php

<?php

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('displayName')
            ->add('plainPassword', RepeatedType::class, [
                'type' => PasswordType::class
            ])
            ->add('avatar',FileType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => User::class,
        ]);
    }
}
10
  • Obviously, a File instance cannot be serialized. Probably, your User object should not contain an instance of File?
    – Nico Haase
    Apr 11, 2018 at 18:44
  • Yes that's right, I'm storing just the name of the file. But according to symfony documentation, I should pass the File type instead of the file name(string). Please check this out: symfony.com/doc/current/controller/upload_file.html
    – Soheil
    Apr 11, 2018 at 18:47
  • No, your application is not written according to some documentation, but tailored to your needs.
    – Nico Haase
    Apr 11, 2018 at 18:48
  • Would you please explain more? Which part you mean?
    – Soheil
    Apr 11, 2018 at 18:51
  • You cannot write a whole application following only documentation. If putting a File into your form causes an error, then don't do it. And if adding Serializable to the entity class solves that error, but causes another, you should adjust your question
    – Nico Haase
    Apr 11, 2018 at 18:55

8 Answers 8

13

After some debugging I found the solution myself.

The problem is, when User Entity was implementing the UserInterface, the user provider(actually the Doctrine, behind the scene) tried to Serializing the User object to store it in the session but because of the file that I assigned it to this class, it fails it's career!

To solve the problem, first I tried to fetch separate User object from database but unfortunately Doctrine gave me the exact reference of the User object again.(That's not a bug. Thanks to Doctrine. It's too smart to query as less as possible).

Second, I clone the User object myself in the controller before sending it to the UserType form, and then everything went well.

But that is not the best practice because you may have some other problems with registration, profile update or other scenarios that you may have with User class.

In my application, I added another entity called Media and it stores the files with the file system and each entity like User which need some media (Like user avatar here), just have a ManyToOne relationship with this entity. In this case you can just save the name file as string in avatar field in User class.

You may have some other designs in your application but as I experienced, Do not assign a File field directly to the User entity which is implementing UserInterface!

2
  • Hello can you help me more to understand your solution
    – Dah
    May 22, 2019 at 15:23
  • Thank you so much for this explanation, you really saved my ass haha. Kudos! Aug 2, 2020 at 19:01
3

I've done something like this:

class User implements UserInterface, Serializable {

    // our image
    private $profileImage;

    /*
    Rest of our awesome entity
    */

    public function serialize()
    {
        $this->profileImage = base64_encode($this->profileImage);
    }

    public function unserialize($serialized)
    {
        $this->profileImage = base64_decode($this->profileImage);

    }
}

And it's working pretty well.

1

Very good answer Sohell, you summed it up perfectly.

I had the same problem and found a workaround, after flushing the entityManager, I just set the imageFile property of the user object to null.

It let's the user object update and persist the right object in session, the File object excluded

1

This may help someone, the issue is actually not so much of a big deal. Simply add the @Ignore annotation on the File field in your entity - fixed the issue for me.

Example:

/**
 * @Vich\UploadableField(mapping="users", fileNameProperty="imageName")
 * @Ignore()
 */
 private ?File $imageFile = null;
0
0

Be carefull ! If you have some mistakes in your form, symfony will still try to serialize your uploaded file on form page rendering. I had to set to null imageFile if $form->isValid() return false too.

0

1. First error

Serialization of 'Symfony\Component\HttpFoundation\File\File' is not allowed

  • this is because of this instruction in profileAction(): you assigned the File object to the avatar field ... which became not serializable.
    public function profileAction(Request $request) 
     { 
         $user = $this->getUser();       
         $user->setAvatar( 
           new File(Constants::UPLOAD_AVATAR.'/'.$user->getAvatar())
         );
        ...
    }

You cannot also set the avatar field like that anytime you enter profileAction() function ... this recursively change the name, adding prefix directory ahead the name.

2. security error, back to login page, anon. authentication

If you correctly remove the serialization problem, the user authentication can proceed normally to authenticated status ... If you have problem again with login form loop with anon. status, you may have to implement EquatableInterface as now required in SF4.

Hope this help you.

2
  • 1
    Thanks for the help. The code that you copied here from the controller is the exact code of Symfony Documentation. Please check the URLs in the question and the comments. For your second answer the problem was User Provider behaviour. Please check the answer that I've just posted for this question for more info.
    – Soheil
    Apr 23, 2018 at 5:09
  • So, the Symfony code was wrong... and you confirmed it by writing : " Do not assign a File field directly to the User entity which is implementing UserInterface!"
    – kasor
    Apr 24, 2018 at 9:06
0

In the User entity:

/**
 * @ORM\OneToMany(targetEntity="UserFile",mappedBy="user", cascade={"remove"})
 * @ORM\OrderBy({"created_at" = "DESC"})
 */
protected $userfiles;

The Userfile class:

<?php


namespace App\Entity;

use App\Repository\UserFileRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Vich\UploaderBundle\Entity\File as EmbeddedFile;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

/**
 * @ORM\Entity(repositoryClass=UserFileRepository::class)
 * @Vich\Uploadable
 * @ORM\HasLifecycleCallbacks
 */
class UserFile implements \Serializable
{
    /**
     * Constructor
     */
    public function __construct()
    {
        // voor de VichUploader
        $this->image = new EmbeddedFile();
    }

    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="User", inversedBy="userfiles")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
     * @ORM\OrderBy({"achternaam" = "DESC"})
     * @Assert\NotBlank()
     */
    protected $user;

    /**
     * @var string
     * @ORM\Column(name="type", type="string", length=20, nullable=true)
     */
    protected $type;

    //--------------------------------------------------------------------------------

    /**
     * @ORM\Column(name="created_at", type="datetime", nullable=true)
     *
     */
    protected $created_at;

    /**
     * @ORM\Column(name="updated_at", type="datetime", nullable=true)
     *
     */
    protected $updated_at;

    //--------------------------------------------------------------------------------
    // de kolommen voor het uploaden van een image via VichUploader
    //--------------------------------------------------------------------------------

    /**
     * NOTE: This is not a mapped field of entity metadata, just a simple property.
     *
     * @Vich\UploadableField(mapping="gebruiker_foto",
     *     fileNameProperty="image.name",
     *     size="image.size",
     *     mimeType="image.mimeType",
     *     originalName="image.originalName",
     *     dimensions="image.dimensions")
     *
     * @var File|null
     */
    private $imageFile;

    /**
     * @ORM\Embedded(class="Vich\UploaderBundle\Entity\File")
     * @var EmbeddedFile
     */
    private $image;


    //--------------------------------------------------------------------------------

    public function getId(): ?int
    {
        return $this->id;
    }

    //--------------------------------------------------------------------------------

    public function getUser() : ?User
    {
        return $this->user;
    }

    public function setUser(?User $user)
    {
        $this->user = $user;
        return $this;
    }

    /**
     * @return string
     */
    public function getType(): ?string
    {
        return $this->type;
    }

    /**
     * @param string $type
     * @return UserFile
     */
    public function setType(?string $type): self
    {
        $this->type = $type;
        return $this;
    }

    //--------------------------------------------------------------------------------

    /**
     *
     */
    public function getCreatedAt()
    {
        return $this->created_at;
    }

    /**
     *
     */
    public function getUpdatedAt()
    {
        return $this->updated_at;
    }

    //--------------------------------------------------------------------------------
    // callbacks
    //--------------------------------------------------------------------------------

    /**
     * @ORM\PrePersist()
     * Hook on pre-persist operations
     */
    public function prePersist()
    {
        $this->created_at = new \DateTime;
        $this->updated_at = new \DateTime;
    }

    /**
     * @ORM\PreUpdate()
     * Hook on pre-update operations
     */
    public function preUpdate()
    {
        $this->updated_at = new \DateTime;
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function verkleinFoto()
    {
        if (null === $this->imageFile) {
            return;
        }

        // create smaller image, wel graag vierkante plaatjes!!
        $width    = 160;
        $imagine  = new \Imagine\Gd\Imagine();
        $image    = $imagine->open($this->imageFile);
        $size     = $image->getSize();

        $image->resize($size->widen($width));
        $realpath = $this->imageFile->getRealPath();
        $image->save($realpath);

        // if there is an error when moving the file, an exception will
        // be automatically thrown by move(). This will properly prevent
        // the entity from being persisted to the database on error
        //$this->file->move($this->getUploadRootDir(), $this->path);

        //unset($this->file);
    }

    //--------------------------------------------------------------------------------
    // de setters/getters voor het uploaden van een image via VichUploader
    //--------------------------------------------------------------------------------

    /**
     * If manually uploading a file (i.e. not using Symfony Form) ensure an instance
     * of 'UploadedFile' is injected into this setter to trigger the  update. If this
     * bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
     * must be able to accept an instance of 'File' as the bundle will inject one here
     * during Doctrine hydration.
     *
     * @param File|UploadedFile|null $imageFile
     */
    public function setImageFile(?File $imageFile = null): void
    {
        $this->imageFile = $imageFile;

        if (null !== $imageFile) {
            // It is required that at least one field changes if you are using doctrine
            // otherwise the event listeners won't be called and the file is lost
            $this->updated_at = new \DateTimeImmutable();
        }
    }

    public function getImageFile(): ?File
    {
        return $this->imageFile;
    }

    public function setImage(EmbeddedFile $image): void
    {
        $this->image = $image;
    }

    public function getImage(): ?EmbeddedFile
    {
        return $this->image;
    }

    //--------------------------------------------------------------------------------
    public function serialize()
    {
        $this->imageFile = base64_encode($this->imageFile);
    }

    public function unserialize($serialized)
    {
        $this->imageFile = base64_decode($this->imageFile);

    }

}
0

Just recently again active with symfony, working my way through symfony 5.

But what I did was following:

  1. created the User entity implementing Userinterface
  2. Added an entity Userfile and made the many to one relation in the User entity
  3. Made that class Serializable (and that is the trick)

If you do that, you can add files/photo's while you're authenticated, and won't get that error

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.