Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Collection Delete & allow_delete

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

Right now, this Genus is related to four GenusScientists. Cool... but what if one of those users stopped studying the Genus - how could we remove that one?

The Delete UI & JavaScript

Let's plan out the UI first: I want to be able click a little x icon next to each embedded form to make it disappear from the page. Then, when we submit, it should fully delete that GenusScientist record from the database. Cool?

Inside the embedded form, add a new class to the column: js-genus-scientist-item:

{{ form_start(genusForm) }}
... lines 2 - 24
{% for genusScientistForm in genusForm.genusScientists %}
<div class="col-xs-4 js-genus-scientist-item">
... lines 27 - 32
</div>
{% endfor %}
... lines 35 - 37
{{ form_end(genusForm) }}

We'll use that in JavaScript in a second. Below that, add a little link with its own js-remove-scientist class... and put the cute little "x" icon inside:

{{ form_start(genusForm) }}
... lines 2 - 24
{% for genusScientistForm in genusForm.genusScientists %}
<div class="col-xs-4 js-genus-scientist-item">
<a href="#" class="js-remove-scientist pull-right">
<span class="fa fa-close"></span>
</a>
... lines 30 - 32
</div>
{% endfor %}
... lines 35 - 37
{{ form_end(genusForm) }}

Brilliant!

Time to hook up some JavaScript! Since this template is included by edit.html.twig and new.html.twig, I can't override the javascripts block from here. Instead, open edit.html.twig and override the block javascripts there:

... lines 1 - 2
{% block javascripts %}
{{ parent() }}
... lines 5 - 18
{% endblock %}
... lines 20 - 32

We'll worry about adding JS to the new template later.

Start with the always-exciting document.ready function:

... lines 1 - 2
{% block javascripts %}
{{ parent() }}
<script>
jQuery(document).ready(function() {
... lines 8 - 16
});
</script>
{% endblock %}
... lines 20 - 32

Oh, but back in _form.html.twig, add one more class to the row that's around the entire section called js-genus-scientist-wrapper:

{{ form_start(genusForm) }}
... lines 2 - 23
<div class="row js-genus-scientist-wrapper">
... lines 25 - 34
</div>
... lines 36 - 37
{{ form_end(genusForm) }}

Ok, back to the JavaScript! Add var $wrapper = then use jQuery to select that wrapper element. Register a listener on click for any .js-remove-scientist element - that's the delete link. Start that function with my favorite e.preventDefault():

... lines 1 - 2
{% block javascripts %}
... lines 4 - 5
<script>
jQuery(document).ready(function() {
var $wrapper = $('.js-genus-scientist-wrapper');
$wrapper.on('click', '.js-remove-scientist', function(e) {
e.preventDefault();
... lines 12 - 15
});
});
</script>
{% endblock %}
... lines 20 - 32

Then... what next? Well, forget about Symfony and the database: just find the .js-genus-scientist-item element that's around this link and... remove it!

... lines 1 - 2
{% block javascripts %}
... lines 4 - 5
<script>
jQuery(document).ready(function() {
var $wrapper = $('.js-genus-scientist-wrapper');
$wrapper.on('click', '.js-remove-scientist', function(e) {
e.preventDefault();
$(this).closest('.js-genus-scientist-item')
.fadeOut()
.remove();
});
});
</script>
{% endblock %}
... lines 20 - 32

Simple! Refresh the page, click that "x", and be amazed.

Missing Fields: The allow_delete Option

But this is superficial: it didn't delete anything from the database nor can we submit the form and expect something to magically delete this GenusScientist, just because we removed it from the page. Or can we?

Submit! Well, I guess not. Huge error from the database!

UPDATE genus_scientist SET years_studied and user_id to null.

Hmm. So our form is not expecting this embedded form to simply disappear. Instead, because the fields are missing from the submitted data, it thinks that we want to set that Genus Scientist's yearsStudied and user fields to null! No! I want to delete that entire object from the database!

How can we do that? First, in GenusFormType, we need to tell the genusScientists field that it's ok if one of the embedded form's fields is missing from the submit. Set a new allow_delete option to true:

... lines 1 - 18
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 24 - 46
->add('genusScientists', CollectionType::class, [
... line 48
'allow_delete' => true,
])
;
}
... lines 53 - 59
}

This tells the CollectionType that it's ok if one of the GenusScientist forms is missing when we submit. And, if a GenusScientist form is missing, it should remove that GenusScientist from the genusScientists array property. In other words, when we remove a GenusScientist form and submit, the final array will have three GenusScientist objects in it, instead of four.

Ready? Submit!

Hmm, no error... but it still doesn't work. Why not? Hint: we already know the answer... and it relates to Doctrine's inverse relationships. Let's fix it.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

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

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice