Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Data Persister: Encoding the Plain Password

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When an API client makes a POST request to /api/users, we need to be able to run some code after API Platform deserializes the JSON into a User object, but before it gets saved to Doctrine. That code will encode the plainPassword and set it on the password property.

Introducing Data Persisters

How can we do that? One great answer is a custom "data persister". OooooOOOo. API Platform comes with only one data persister out-of-the-box, at least, only one that we care about for now: the Doctrine data persister. After deserializing the data into a User object, running security checks and executing validation, API Platform finally says:

It's time to save this resource!

To figure out how to save the object, it loops over all of its data persisters... so... really... just one at this point... and asks:

Hi data persister! Do you know how to "save" this object?

Because our two API resources - User and CheeseListing are both Doctrine entities, the Doctrine data persister says:

Oh yea, I totally do know how to save that!

And then it happily calls persist() and flush() on the entity manager.

This... is awesome. Why? Because if you want to hook into the "saving" process... or if you ever create an API Resource class that is not stored in Doctrine, you can do that beautifully with a custom data persister.

Check it out: in the src/ directory - it doesn't matter where - but let's create a DataPersister/ directory with a new class inside: UserDataPersister.

This class will be responsible for "persisting" User objects. Make it implement DataPersisterInterface. You could also use ContextAwareDataPersisterInterface... which is the same, except that all 3 methods are passed the "context", in case you need the $context to help your logic.

... lines 1 - 4
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
class UserDataPersister implements DataPersisterInterface
{
... lines 9 - 22
}

Anyways I'll go to the Code -> Generate menu - or Command+N on a Mac - and select "Implement Methods" to generate the three methods this interface requires.

... lines 1 - 8
public function supports($data): bool
{
// TODO: Implement supports() method.
}
public function persist($data)
{
// TODO: Implement persist() method.
}
public function remove($data)
{
// TODO: Implement remove() method.
}
... lines 23 - 24

And... we're... ready! As soon as you create a class that implements DataPersisterInterface, API Platform will immediately start using that. This means that, whenever an object is saved - or removed - it will now call supports() on our data persister to see if we know how to handle it.

In our case, if data is a User object, we do support saving this object. Say that with: return $data instanceof User.

... lines 1 - 17
public function supports($data): bool
{
return $data instanceof User;
}
... lines 22 - 35

As soon as API Platform finds one data persister whose supports() returns true, it calls persist() on that data persister and does not call any other data persisters. The core "Doctrine" data persister we talked about earlier has a really low "priority" in this system and so its supports() method is always called last. That means that our custom data persister is now solely responsible for saving User objects, but the core Doctrine data persister will still handle all other Doctrine entities.

Saving in the Data Persister

Ok, forget about encoding the password for a minute. Now that our class is completely responsible for saving users... we need to... yea know... make sure we save the user! We need to call persist and flush on the entity manager.

Add public function __construct() with the EntityManagerInterface $entityManager argument to autowire that into our class. I'll hit my favorite Alt + Enter and select "Initialize fields" to create that property and set it.

... lines 1 - 6
use Doctrine\ORM\EntityManagerInterface;
... line 8
class UserDataPersister implements DataPersisterInterface
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
... lines 17 - 33
}

Down in persist(), it's pretty simple: $this->entityManager->persist($data) and $this->entityManager->flush(). Data persisters are also called when an object is being deleted. In remove(), we need $this->entityManager->remove($data) and $this->entityManager->flush().

... lines 1 - 22
public function persist($data)
{
$this->entityManager->persist($data);
$this->entityManager->flush();
}
public function remove($data)
{
$this->entityManager->remove($data);
$this->entityManager->flush();
}
... lines 34 - 35

Congrats! We now have a data persister that... does exactly the same thing as the core Doctrine data persister! But... oh yea... now, we're dangerous. Now we can encode the plain password.

Encoding the Plain Password

To do that, we need to autowire the service responsible for encoding passwords. If you can't remember the right type-hint, find your terminal and run:

php bin/console debug:autowiring pass

And... there it is: UserPasswordEncoderInterface. Add the argument - UserPasswordEncoderInterface $userPasswordEncoder - hit "Alt + Enter" again and select "Initialize fields" to create that property and set it.

... lines 1 - 7
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
... line 9
class UserDataPersister implements DataPersisterInterface
{
... line 12
private $userPasswordEncoder;
... line 14
public function __construct(EntityManagerInterface $entityManager, UserPasswordEncoderInterface $userPasswordEncoder)
{
... line 17
$this->userPasswordEncoder = $userPasswordEncoder;
}
... lines 20 - 46
}

Now, down in persist(), we know that $data will always be an instance of User. ... because that's the only time our supports() method returns true. I'm going to add a little PHPdoc above this to help my editor.

Hey PhpStorm! $data is a User! Ok?,

... lines 1 - 5
use App\Entity\User;
... lines 7 - 9
class UserDataPersister implements DataPersisterInterface
{
... lines 12 - 25
/**
* @param User $data
*/
public function persist($data)
... lines 30 - 46
}

Let's think. This endpoint will be called both when creating a user, but also when it's being updated. And... when someone updates a User record, they may or may not send the plainPassword field in the PUT data. They would probably only send this if they wanted to update the password.

This means that the plainPassword field might be blank here. And if it is, we should do nothing. So, if $data->getPlainPassword(), then $data->setPassword() to $this->userPasswordEncoder->encodePassword() passing the User object - that's $data - and the plain password: $data->getPlainPassword().

That's it friends! Well, to be extra cool, let's call $data->eraseCredentials()... just to make sure the plain password doesn't stick around any longer than it needs to. Again, this is probably not needed because this field isn't saved to the database anyways... but it might avoid the plainPassword from being serialized to the session via the security system.

... lines 1 - 28
public function persist($data)
{
if ($data->getPlainPassword()) {
$data->setPassword(
$this->userPasswordEncoder->encodePassword($data, $data->getPlainPassword())
);
$data->eraseCredentials();
}
... lines 37 - 39
}
... lines 41 - 48

And... done! Aren't data persisters positively lovely?

Oh, well, we're not quite finished yet. The field in our API is still called plainPassword... but we wrote our test expecting that it would be called just password... which I kinda like better.

No problem. Inside User, find the plainPassword property and give it a new identity: @SerializedName("password").

... lines 1 - 13
use Symfony\Component\Serializer\Annotation\SerializedName;
... lines 15 - 36
class User implements UserInterface
{
... lines 39 - 78
/**
... line 80
* @SerializedName("password")
*/
private $plainPassword;
... lines 84 - 216
}

Let's check that on the docs... under the POST operation... perfect!

So... how can we see if this all works? Oh... I don't know... maybe we can run our awesome test!

php bin/phpunit --filter=testCreateUser

Above all the noise.. we got it!

Next, our validation rules around the plainPassword field... aren't quite right yet. And it's trickier than it looks at first: plainPassword should be required when creating a User, but not when updating it. Duh, duh, duh!

Leave a comment!

105
Login or Register to join the conversation
Laurent Avatar
Laurent Avatar Laurent | posted 3 years ago

Hi SymfonyCast team,

I was wondering why you did not use an event instead of data persister to hook and crypt password (eg : PRE_WRITE event) ?

Regards

2 Reply

Hey Willems!

Good question :). And I didn't have any specific reason behind this. The truth is that data persisters were the way that occurred to me first - then later I realized that an event listener can also accomplish this. I can't think of any practical difference between these two options. As a side note, I *had* planned to show events in a future tutorial - mentioning PRE_WRITE as a parallel option to a data persister might be a good thing to mention there.

Cheers!

Reply

Hi again weaverryan!

Actually, I'm going to modify my statement and say that there IS at least one difference between using a data persister versus the PRE_WRITE event. A data persister will work both for the REST API *and* also if you start using GraphQL with API Platform. The PRE_WRITE event, however, is specific to the REST API. So, the data persister is a bit more powerful because it's used in more places :).

Cheers!

Reply

There's a situation where a Doctrine event on PrePersist might not work with GraphQL? The data persister isn't triggered by my fixtures for my tests so, here's another difference I guess.

Reply

Hey @Jean-Nicolas Lagneau!

Hmm. No, there should be no difference when it comes to Doctrine event and GraphQL. But REST and GraphQL modify objects and then save them via Doctrine. And this should trigger the PrePersist identically. Now, while I'm pretty sure I'm right, full disclosure, I have not tried this in GraphQL, so I could be wrong.

The most common reason for this part to "go wrong" is that no "persisted properties" are modified on your User object. And so, when it's saved, Doctrine is "smart enough" to realize that no persisted properties were changed, and so it skips saving. That can happen if, for example, you make a GraphQL request to update the "password" property only. The problem would be that this updates the plainPassword property, which is not a persisted property... and so then Doctrine thinks that nothing needs to save. If you have this situation, you need to modify some persisted property from inside of setPlainPassword(). An easy thing to do is have an updatedAt field where you set it in that method:


public function setPlainPassword(string $password)
{
    $this->plainPassword = $password;
    // this will trigger the full save cycle to happen
    $this->updatedAt = new \DateTime();
}

Let me know if that helps!

Cheers!

Reply
Startchurch Avatar

I saw this after I asked my question

Reply
Daniel-G Avatar
Daniel-G Avatar Daniel-G | posted 9 months ago | edited

Hallo everyone,

for people, they do this tutorial with symfony 6 and API-Platform version 3. If I am right, the "Data Persister" is changed to "Processor" and the code is a little bit different. You can see the documention here: https://api-platform.com/docs/main/core/state-processors/

Here is my code regarding this tutorial

// src/State/UserDataProcessor.php
// This base file was created with "php bin/console make:state-processor"
<?php
namespace App\State;

use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class UserDataProcessor implements ProcessorInterface
{

    /**
     * @var EntityManagerInterface $entityManagerInterface
     */
    private $entityManagerInterface;

    /**
     * @var UserPasswordHasherInterface $userPasswordHasherInterface
     */
    private $userPasswordHasherInterface;

    public function __construct(EntityManagerInterface $entityManagerInterface,
                    UserPasswordHasherInterface $userPasswordHasherInterface
        )
    {
        $this->entityManagerInterface = $entityManagerInterface;
        $this->userPasswordHasherInterface = $userPasswordHasherInterface;
    }

    /**
     * @param User $data
     */
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
    {
        if ($operation instanceof DeleteOperationInterface) {
            $this->entityManagerInterface->remove($data);
            $this->entityManagerInterface->flush();
            return;
        }
        if($data->getPlainPassword()) {
            $data->setPassword(
                $this->userPasswordHasherInterface->hashPassword($data, $data->getPlainPassword())
            );

            $data->eraseCredentials();
        }
        $this->entityManagerInterface->persist($data);
        $this->entityManagerInterface->flush();
        return $data;

    }
}

And you need to change the User Entity and add processor: UserDataProcessor::class in your ApiResource

 <?php

namespace App\Entity;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use App\Repository\UserRepository;
use App\State\UserDataProcessor;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ApiResource(
    normalizationContext: ["groups" => ['user:read']],
    denormalizationContext: ["groups" => ['user:write']],
    processor: UserDataProcessor::class /// This is important to merge the both files together.
)]
#[Post(security: "is_granted('ROLE_ADMIN')")]
#[Get()]
#[DELETE(security: "is_granted('ROLE_ADMIN')")]
#[PUT()]
#[PATCH()]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
...
}

Maybe it helps anyone.

To Symfony Cast team. Thanks for the good documentation and video. :)

1 Reply
Nikrou Avatar

My project used symfony 6.0.15 and Api platform 2.7 and the processor way is not yet available.

But the datapersister way does not work. The method persist is called but the plain password field is always empty. I cannot figure what the problem is. Any help appreciate.

My code is almost the same as here : same dataPersister, serializedName for plain passord. The request contains the field password and I update over field at the same time to be sure the object is updated for doctrine point of view. No way, the plain password of my entity is always empty !

Reply
Daniel-G Avatar

Hey Nikrou,

I did used api-platform 3.0.4 and Symfony 6.1.
And just in this moment I tested with symfony 6.0 and 2.7. Is does work very well.

in which step is your plainPassword empty? Because in the end of the progress it has to be empty. This would be make sure by method $data->eraseCredentials();

Do you have a repository of your project? Or can you share your User entity class and your Data Persister class at least. :-)

Reply
Nikrou Avatar

Thanks for your quick answer.
I post a gist : https://gist.github.com/nikrou/73df58d0dd9a6ff9ad53767a64d82fa8
I removed not revelant par of User entity class but I can post the whole file if you prefered.

The plain password is empty in the persist method of UserDataPersister, before test on line 25 (if $data->getPlainPassword ...)

Reply
Daniel-G Avatar

I found it. check this line:
https://gist.github.com/nikrou/73df58d0dd9a6ff9ad53767a64d82fa8#file-userdatapersister-php-L26

You set the plain password not the password ;-)

You should write $data->setPassword($this->passwordHasher->hashPassword($data, $data->getPlainPassword()));

1 Reply
Nikrou Avatar

Unfortunately that's not the problem. But you're right it must be setPasswod and not setPlainPassword.

But the test on line 25 is false because the plain password is empty (null). I cannot proved it but my idea is that the SerializedName annotation is ignored or not worked as expected. The denormalization process to not populate the plain password field based on the password in the request.

Reply
Daniel-G Avatar

I use your this part of your User Class too. But it is still work.
Maybe there are problems with implements \Serializable Class. But I am not sure.

Here is my test environment. Maybe it helps you to find the failure.
https://gitlab.com/grabasch/DataPersister

Reply
Nikrou Avatar

Your plain password field must not be a reat field in the database. You must remove the @ORM annotation. Of course with that I imagine it works but it's not the proper way ! :-)

2 Reply
DustinUtecht Avatar
DustinUtecht Avatar DustinUtecht | Nikrou | posted 8 months ago | edited

I have a similar issue.
The prop on my user entity looks like this:

 #[Groups(['write'])]
    #[SerializedName("password")]
    private ?string $plainPassword;

My UserProcessor is c&p of your code above, but if i try to add a new user i get this error message:

Typed property App\Entity\User::$plainPassword must not be accessed before initialization

If i initialize the prop as null

#[Groups(['write'])]
    #[SerializedName("password")]
    private ?string $plainPassword = null;

I have the same issue like Nikrou, the plainPassword prop is always null/empty.

1 Reply
Daniel-G Avatar
Daniel-G Avatar Daniel-G | Nikrou | posted 8 months ago | edited

Yes thanks, I don't remove the annotation. You are totally right it is not good to have a plain password attributed in the DB Table. But I tested after I removed this ORM annotation and it is still work. This is also not the reason for the problem or did you solved this issue with this work around?

I updated my repository.

Reply
Nikrou Avatar

I do not understand but it works ! I will start with your project and add the difference from mine to try to find the problem.
Anyway, many thanks for your help. I will post of course the solution when I will find id !

1 Reply
Daniel-G Avatar

You are welcome and very good that it works now, this sounds very good. And I am very interested what are the different. :) maybe you will find the reason or other one has an idea :)

Reply
Nikrou Avatar

After many changes, I finally found what was the problem. My plain password field property was $plain_password and my setter was setPlainPassword. I changed my property to $plainPassword and it works now !

Reply
DustinUtecht Avatar
DustinUtecht Avatar DustinUtecht | Daniel-G | posted 8 months ago | edited

Is you're composer.lock up to date ?
I tried you're repository, but it dont work for me!
I did this:

  1. git clone https://gitlab.com/grabasch/DataPersister.git
  2. composer install
  3. symfony console make:migration
  4. symfony console doctrine:migrations:migrate
  5. Send a POST request
curl -X 'POST' \
  'https://127.0.0.1:8001/api/users' \
  -H 'accept: application/ld+json' \
  -H 'Content-Type: application/ld+json' \
  -d '{
  "email": "test@test.de",
  "password": "test123"
}'

And i get this error:

An exception occurred while executing a query: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'password' cannot be null

Edit: Your demo repository uses UserDataPersister instead of a Processor.

Reply

I just checked this repo, and went through the steps, except the POST request was made via postman and I don't see any issues

1 Reply
DustinUtecht Avatar
DustinUtecht Avatar DustinUtecht | sadikoff | posted 8 months ago

Strange, i tested it again at home with my linux system and it workes fine.
But as i wrote it don't worked for me on my mac at work.
Seems some kind of strange OS/Enviroment issue.

Reply
Daniel-G Avatar
Daniel-G Avatar Daniel-G | DustinUtecht | posted 8 months ago | edited

Hi DustinUtecht,

here is repository with current versions Symfony 6.1 and API-Platform 3.0
It should work also fine.
https://gitlab.com/symfony-grabasch/symfonycast/dataprocessor

Maybe it helps.

I want to add the repository link to my first message. But I can't edit. Maybe a SymfonyCasts staff can add this repo link to my first message. :)

Reply
Daniel-G Avatar

Hey DustinUtecht,

yes the repository not the current version. It is regarding the error from Nikrou. He use symfony v6.0 and api Platform v2.7 because it seems that api platform 3.0 is not available for symfony 6.0.

My code in my first message is for the current symfony version 6.1.

If you want I can create a repository with current version, this evening. :)

Did you check e.g. with dump on Processor::process() which values are in $data?

Reply
DustinUtecht Avatar
DustinUtecht Avatar DustinUtecht | Daniel-G | posted 8 months ago | edited

Thank you for the new Repository, it works fine for me (on linux).
But as written in the reply to sadikoff on linux your other repository also works fine.
Seems some kind of strange OS/Enviroment issue.

I cloned the app from work and tested it under linux and it still don't work even if i update to api platform 3.
Just the plainPassword prop is null, all other props are filled.
As example i send this via axios:

{"roles":["ROLE_ADMIN"],"firstname":"saf","lastname":"asf","email":"fas@fasf.de","telephone":"0259468485434","password":"test123"}

What would be the best way to figure out why it don't work ?

Reply

Hey Dustin!

That is super weird. How to debug? I am just entering this conversation after a lot of nice messages, so I may be missing some context. If you're using the data processor code, I guess the question is: is the process() method being called at all when you make this request?

Cheers!

Reply
DustinUtecht Avatar

Hey weaverryan, it seems i forgott to edit my post yesterday.
I found the problem, it was.... me. I accendently switched the groups in the (de)normalizationContext.

Reply

No worries - I'm just happy you got it sorted out :)

Reply

Hey @Daniel-G

Thanks for posting this - VERY awesome of you :). We're going to be hard at work in December updating the tutorials for API Platform 3. However, I talked with the API Platform lead dev last week and he assured me that, because we taught everything "the right way" for these tutorials (this was not an accident - we had him make sure we were doing things correctly!), going to API Platform 3 is mostly new namespaces and new names. This is a perfect example: "data persisters" are, indeed, "state processors" in API Platform 3, which is mostly a name change. Same for "data providers" -> "state providers".

Cheers!

Reply
Evozon S. Avatar
Evozon S. Avatar Evozon S. | posted 2 years ago

Hello

I'm leaving some alternative extension points just in case.
https://api-platform.com/do...
https://api-platform.com/do...

1 Reply

Hey Victor,

Thanks for sharing some useful links to the official docs, it might help to understand concepts better from a different point of view :)

Cheers!

Reply

I'm wondering why you chose not to decorate the default persister, but instead implement yours fully. Decorating seems like a cleaner choice to me, plus it allows you to be decoupled from the actual storage, don't you think?

Reply

Hey Rimas,

Good question! Fairly speaking, I'm not sure 100%, probably Ryan wanted to show the flexibility here, that's why a completely custom persister would be more flexible and probably simpler from the implementation point of view. But decorating the default persister sounds cool to me too, good catch! And agree, it would be a cleaner way IMO too. Feel free to do this way instead :)

Cheers!

Reply

Well, as I progress throgh this course, my question just got answered: Ryan rewrites this persister in Part 3 Chapters 2 and 3 to use the decorator pattern. 🙃

So you could say I'm ahead of schedule. Aren't I cool! 😎

Reply

Hey Rimas,

Ah, you're definitely cool! ;) Yeah, that's great you were able to see the potential of using decorator pattern in advance, well done! :)

Cheers!

Reply
Daniel U. Avatar
Daniel U. Avatar Daniel U. | posted 1 year ago

Hi Team,

I was wondering what's the standard for the custom data persister is we don't want to allow to remove an object. Do we return an exception in the remove function?

Regards

Reply

Hey Daniel,

I think you can do that but if something is calling the remove method of your data persister when it should not, it means you have a problem else where. What I mean is if you feel like something or someone can call that method without your consent, then, just throw an exception right there but if that's not the case, disabling the remove method for such ApiResource should be good enough

Cheers!

Reply
Default user avatar
Default user avatar Jascha Lukas Gugat | posted 2 years ago

Hi symfony Team,
i got a little stuck on combining the previously used embedded write functionality with contextawaredatapersisters.

i am using a mapped superclass called baseEntity with properties all of my entities should have like uuid, createdBy, createdAt etc. The values for these properties are generated by using the baseEntites datapersister and it works fine if i am posting for example a bookEntity which extends the baseEntity. It is also working for a bookCommentEntity extending also the baseEntity and mapped to the bookEntity. But if i am using the embedded write functionality to directly create a bookComment on posting a new book with cascade persist and denormalization groups the bookComment is created successfully but without the generated uuid, createdBy, and createdAt values. The baseEnties datapersister is simply not called for the commentEntities that are generated by the embedded write cascade. Is there something miss-configured or is this behaviour expected for a datapersister and if so how can i ensure that the datapersister is also applied to the creation process of these entities?

Regards

Reply

Hi Jascha Lukas Gugat!

> The baseEnties datapersister is simply not called for the commentEntities that are generated by the embedded write cascade. Is there something miss-configured or is this behaviour expected for a datapersister and if so how can i ensure that the datapersister is also applied to the creation process of these entities?

No, you nailed it: a custom data persister (and the same is true for data providers) is only called for the main, top-level resource. It makes sense when you think about if from API Platform's perspective: at the end of the request, API Platform says "Someone save this object for me!". And then the data persister system takes over. But if that object has embedded objects, that is entirely up to whatever data persister is handling the object to deal with (e.g. the Doctrine data persister of course handles saving embedded objects).

My advice would be to actually set these properties a different way. This is... motivated in part by "how can we solve this problem" but it's also motivated by the fact that low-level fields (like uuid, createdAt and createdBy) feel more appropriate to me as actual Doctrine listeners. The big reason is that I want these set 100% of the time, regardless of whether an entity is being created through my API, via custom code, via a custom console command, etc.

So, that's my advice: solve this with a Doctrine event listener :).

I hope this helps! Cheers!

Reply
Petru L. Avatar
Petru L. Avatar Petru L. | posted 2 years ago | edited

Hey there , i got a name field with @Assert/NotBlank which gets triggered before the DataPersister, causing: "name: This value should not be null.". Adding DisableAutoMapping in the annotation made no difference. Any idea how can i avoid this and still keep the constraint?

Reply

Hey Petru L.!

Hmm. Yea, you should remove the @Assert\NotBlank :). That may sound wrong at first. But if you are not expecting your user to send this field (and you are going to fill it in yourself in a data persister), then from the user's perspective, it is *not* a required field. If you want this field to be required sometimes (e.g. on EDIT) but not others (e.g. CREATE), then you can use validation groups for that.

Let me know how that fits your situation.

Cheers!

Reply
Pedro S. Avatar
Pedro S. Avatar Pedro S. | posted 2 years ago

Hello Everyone!

I'm facing a problem where I've been stuck for a while. I have created a Data Persister that is working fine when I'm running a test making a post request, but it seems that the Data Persister is not even called when I try to make a post request from the browser (or Postman). Any idea what could be wrong?

Many thanks in advance

Reply

Hey Pedro S.

Could you double check the support() method returns true when it should. And, also check that your DataPersister implements the ApiPlatform\Core\DataPersister\DataPersisterInterface interface

Cheers!

Reply
Zongor A. Avatar
Zongor A. Avatar Zongor A. | posted 2 years ago

Hi! How I can update multiple data. I want send objects in collection with PUT, it is possible? (Not working for me)

Reply

Hey Zongor A.

Unfortunately it's not possible by default as I know... but you can use custom operations or to achieve what you need. https://api-platform.com/do...

Cheers!

Reply
triemli Avatar
triemli Avatar triemli | posted 2 years ago | edited

Hi colleagues,
I'm actually stuck with repeated validator.

In the documentation I found this example:

$builder->add('password', RepeatedType::class, [
    'type' => PasswordType::class,
    'invalid_message' => 'The password fields must match.',
    'options' => ['attr' => ['class' => 'password-field']],
    'required' => true,
    'first_options'  => ['label' => 'Password'],
    'second_options' => ['label' => 'Repeat Password'],

Upon a successful form submit, the value entered into both of the “password” fields becomes the data of the password key.

<b>both of the “password” </b>but where is the second fieldname?

I found in the sources of <i>RepeatedType</i>:

public function getBlockPrefix()
{
    return 'repeated';

But I send any variants of 'repeated_password', 'repeatedPassword' it doesn't work for me.

For more I get an error validation: "This form should not contain extra fields." Ok let's add:

 ->add('repeated_password', PasswordType::class, [
                'mapped' => false,
            ])

$form->isValid() //false
Validation <b>false</b>. No errors. Nothing.

I tried to define first_name and second_name properties, but this both goes to "This form should not contain extra fields."

How are you validate password === password_repeated?

Reply

Hey WebAdequate,

This should be validated out of the box when you call "$form->isValid()" in your controller. Why do you want to get the value from that second field? In theory, you don't need it. The $form->isValid() will return true if both password fields match and false if they are not. And *if* they match - the value from the first field will be enough for you, as its' the same as the value from the 2nd field :)

Yes, by default if you add some extra fields to your form - the validation will fail with that "This form should not contain extra fields." error message. Check your HTML form for any custom fields you created manually. Just use form_row('field_name') to render form fields you have in your form type, don't add any extra HTML fields manually.

Cheers!

Reply
triemli Avatar

I didn't use twig form with their magic becuase I use Vue. Vue knows noithing about our twig. I just collect data with vue and sent it with axios. basically i have fields:

username,
password,
and ... unknown username field, repeated_password or maybe _repeated_password?.

User must enter the password two times.

Reply

Hey WebAdequate,

Ah, I see... This is even easier then! Just open Chrome Developer toolbar and inspect the code of your form - you will see all your fields in it and then you will just need to get them in your JS code to be able to get their values and validation.

Well, you can still do validation on server side, e.g. send an AJAX request with the data to the server and use Symfony validator to validate the data. Then return the response saying whether the validation is passed or an array of errors to show to the user using Vue JS.

Anyway, validating such forms on server side is a good idea as it's more secure.

I hope this helps!

Cheers!

Reply
Benjamin K. Avatar
Benjamin K. Avatar Benjamin K. | posted 3 years ago | edited

After a Problem with the DataPersister i noticed my generated ID Getter Method from maker Bundle is ?int. But an ID cant be null or? So make it sense to remove the question mark and after i get a better failure message?
Instead: Unable to generate an IRI for "App\Entity\Checklist". i get then: Return value of App\Entity\Checklist::getId() must be of the type int, null returned

`

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

`

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3, <8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.5
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.6
        "nesbot/carbon": "^2.17", // 2.21.3
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.3.*", // v4.3.2
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/expression-language": "4.3.*", // v4.3.2
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/http-client": "4.3.*", // v4.3.3
        "symfony/monolog-bundle": "^3.4", // v3.4.0
        "symfony/security-bundle": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.6", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "hautelook/alice-bundle": "^2.5", // 2.7.3
        "symfony/browser-kit": "4.3.*", // v4.3.3
        "symfony/css-selector": "4.3.*", // v4.3.3
        "symfony/maker-bundle": "^1.11", // v1.12.0
        "symfony/phpunit-bridge": "^4.3", // v4.3.3
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}
userVoice