If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhen 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.
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.
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.
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 aUser
! 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!
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!
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!
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.
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!
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. :)
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 !
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. :-)
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 ...)
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()));
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.
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
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 ! :-)
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.
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.
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 !
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 :)
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 !
Is you're composer.lock up to date ?
I tried you're repository, but it dont work for me!
I did this:
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
.
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
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.
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. :)
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?
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 ?
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!
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.
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!
Hello
I'm leaving some alternative extension points just in case.
https://api-platform.com/do...
https://api-platform.com/do...
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!
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?
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!
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! 😎
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!
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
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!
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
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!
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?
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!
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
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!
Hi! How I can update multiple data. I want send objects in collection with PUT, it is possible? (Not working for me)
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!
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?
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!
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.
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!
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;
}
`
// 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
}
}
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