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 SubscribeThe User
class has an agreedTermsAt
property that expects a DateTime
object. But, our form has an agreeTerms
field that, on submit, will give us a true/false boolean value. How can we make these work together? As I so often like to say: there are two options.
First, we could be clever! There is no agreeTerms
property on User
. But, we could create a setAgreeTerms()
method on User
. When that's called, we would actually set the agreedTermsAt
property to the current date. We would also need to create a getAgreeTerms()
method that would return a boolean based on whether or not the agreedTermsAt
property was set.
This is a fine solution. But, this is also a good example of how the form system can start to make your life harder instead of easier. When your form and your class don't look the same, sometimes you can find a simple and natural solution. But sometimes, you might need to dream up something crazy to make it all work. If the solution isn't obvious to you, move on to option two: make the field unmapped.
Let's try that: set agreeTerms
to mapped
false
. To force this to be checked, add constraints
set to a new IsTrue()
... because we need the underlying value of this field to be true
, not false
. Set a custom message:
I know, it's silly, but you must agree to our terms
... lines 1 - 14 | |
class UserRegistrationFormType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
... lines 20 - 34 | |
->add('agreeTerms', CheckboxType::class, [ | |
'mapped' => false, | |
'constraints' => [ | |
new IsTrue([ | |
'message' => 'I know, it\'s silly, but you must agree to our terms.' | |
]) | |
] | |
]) | |
; | |
} | |
... lines 45 - 51 | |
} |
Excellent! Thanks to the mapped = false
, the form should at least load. Try it - refresh! Yes! Well... oh boy - our styling is so bad, the checkbox is hiding off the screen! Let's worry about that in a minute.
Thanks to the mapped => false
, the data from the checkbox does not affect our User
object in any way when we submit. No problem: in SecurityController
, let's handle it manually with if (true === $form['agreeTerms']->getData())
. Wait... that looks redundant! We already have form validation that forces the box to be checked. You're totally right! I'm just being extra careful... ya know... for legal reasons.
... lines 1 - 44 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator) | |
{ | |
... lines 47 - 49 | |
if ($form->isSubmitted() && $form->isValid()) { | |
... lines 51 - 56 | |
// be absolutely sure they agree | |
if (true === $form['agreeTerms']->getData()) { | |
... line 59 | |
} | |
... lines 61 - 71 | |
} | |
... lines 73 - 76 | |
} | |
... lines 78 - 79 |
Inside, we could call $user->setAgreedTermsAt()
and pass the current date. Or, we can do something a bit cleaner. Find the setAgreedTermsAt()
method and rename it to agreeTerms()
, but with no arguments. Inside say $this->agreedTermsAt = new \DateTime()
.
... lines 1 - 19 | |
class User implements UserInterface | |
{ | |
... lines 22 - 264 | |
public function agreeTerms() | |
{ | |
$this->agreedTermsAt = new \DateTime(); | |
} | |
} |
This gives us a clean, meaningful method. In SecurityController
, call that: $user->agreeTerms()
.
... lines 1 - 57 | |
if (true === $form['agreeTerms']->getData()) { | |
$user->agreeTerms(); | |
} | |
... lines 61 - 79 |
Ok team, let's try this. Refresh the page. Annoyingly, I still can't see the checkbox. Let's hack that for now: add a little extra padding on this div. There it is!
Register as geordi3@theenterprise.org
, password engage
, hit enter, and... yes! We know the datetime column was just set correctly in the database because it's required.
Here's the big takeaway: whenever you need a field on your form that doesn't exist on your entity, there may be a clever solution. But, if it's not obvious, make the field unmapped and add a little bit of glue code in your controller that does whatever you need.
Later, we'll discuss a third option: creating a custom model class for your form.
Before we move on, try to reload the fixtures:
php bin/console doctrine:fixtures:load
It... explodes! Duh! I made the new agreedTermsAt
field required in the database, but forgot to update it in the fixtures. No problem: open UserFixture
. In the first block, add $user->agreeTerms()
. Copy that, and do the same for the admin users.
... lines 1 - 9 | |
class UserFixture extends BaseFixture | |
{ | |
... lines 12 - 18 | |
protected function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(10, 'main_users', function($i) use ($manager) { | |
... lines 22 - 24 | |
$user->agreeTerms(); | |
... lines 26 - 41 | |
}); | |
... line 43 | |
$this->createMany(3, 'admin_users', function($i) { | |
... lines 45 - 48 | |
$user->agreeTerms(); | |
... lines 50 - 56 | |
}); | |
... lines 58 - 59 | |
} | |
} |
Cool! Try it again:
php bin/console doctrine:fixtures:load
And.... all better!
Next: let's fix the styling in our registration form by creating our very own form theme.
Hey Oliver,
Hm, this sounds like you're using radio buttons instead of checkboxes. Usually, radio buttons allow you to select only one item from the list at the same time. But checkboxes allows you to select more than one item from the list at the same time. Are you sure you're using checkboxes and not radio buttons? Btw, could you show the code responsible for that form field in your form type? Probably you need to add a few specific options for them to work properly.
Cheers!
Hi Victor,
I am using CHECKboxes!
My code in the show.html.twig
<form>
<div class="input-group mb-3">
<input type="text"
name="q"
class="form-control"
value="{{ app.request.query.get('q') }}"
placeholder="finde in Bezeichnung, Bemerkung, Menge und Wareneingang (tt.mm.jjjj) ..."
>
<div class="input-group-append">
<button type="submit"
class="btn btn-outline-secondary">
<span title="Artikel finden" class="fa fa-search"></span>
</button>
</div>
</div>
</form>
</div>
<div class="rechts">
<button class="btn btn-outline-secondary btn-filterung"><span title="Artikel filtern" class="fa fa-filter" onclick="zeigeKategorieauswahl()"></span></button>
<div id="kategorieauswahl">
<form>
<div class="input-group mb-3">
<div class="kategorieauswahl">
<fieldset>
{% for kategorie in kategorien %}
<div class="auswahlelement">
<input type="checkbox" name="auswahl-kategorien" id="{{ kategorie.id }}" value="{{ kategorie.name }}"> {{ kategorie.name }}
</div>
{% endfor %}
</fieldset>
<div class="input-group-kategorie">
<button type="submit"
class="btn btn-outline-secondary btn-ein">
<span title="Artikel finden" class="fa fa-check-circle"></span>
</button>
</div>
<div class="input-group-kategorie">
<button type="submit"
class="btn btn-outline-secondary btn-aus">
<span title="Artikel finden" class="fa fa-minus-circle"></span>
</button>
</div>
</div>
</div>
</form>```
As you can see, I am using two forms. The selections I need come from the second one.
My code from the controller:
//retrieve possible form variables
$q = $request->query->get('q');
$auswahlkategorien = $request->query->get('auswahl-kategorien');
if ($auswahlkategorien) {
$queryBuilder = $repository->getWithSearchForCategories("Dekoration");
$pagination = $paginator->paginate(
$queryBuilder,
$request->query->getInt('page', 1),
10
);
} else {
$queryBuilder = $repository->getWithSearchQueryBuilder($q);
$pagination = $paginator->paginate(
$queryBuilder,
$request->query->getInt('page', 1),
10
);
}```
This $auswahlKategorien is the variable that shows the value of just ONE checkbox instead of all selected.
Thx
Oliver
Hey Oliver!
Sorry for the slow reply! Ah, I missed the fact you create those checkboxes manually. Symfony usually takes care about unique names, but if you add extra fields manually in the template - you should care about unique names.
So yes, you're correct, the field name should be unique for all checkboxes otherwise the last will always overwrite the previous ones. But the radio buttons should work the reverse, they should have the same name because you can only choose the one option from the list.
Well done! I'm happy you were able to solve this problem yourself!
Cheers!
mapped => false might help me a lot when I dont know how to solve. Hopefully I will remember about this option :)
Hey Lijana Z.
It might be easier for you to remember it if you only remember that your form doesn't have to map all of its fields to a class
Cheers!
Hello Knp !
Is it possible to call services from entity ? Like repository or encode password ?
Cheers !
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.2.1
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.6
"symfony/console": "^4.0", // v4.1.6
"symfony/flex": "^1.0", // v1.17.6
"symfony/form": "^4.0", // v4.1.6
"symfony/framework-bundle": "^4.0", // v4.1.6
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.6
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/twig-bundle": "^4.0", // v4.1.6
"symfony/validator": "^4.0", // v4.1.6
"symfony/web-server-bundle": "^4.0", // v4.1.6
"symfony/yaml": "^4.0", // v4.1.6
"twig/extensions": "^1.5" // v1.5.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
"symfony/dotenv": "^4.0", // v4.1.6
"symfony/maker-bundle": "^1.0", // v1.8.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.6
}
}
Hi,
I am having trouble accessing the values of checkboxes. I am using a group of checkboxes - surrounded by a fieldset. In this fieldset every checkbox has the same name but each has an individual id and value. Trying to fetch the values via $request->query->get('nameofcheckbox'); I only get one of the values. But I need them all. All the values are stated in the URL but only the last one makes its way to my controller.
What am I doing wrong?
Thx
Oliver