Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Product CRUD

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

I've mentioned a few times that Stimulus has a sister technology called Turbo... both of which live under this brand called "Hotwire", which, as we've learned, is all about returning HTML from your server.

Turbo: You'll Write Less Custom JavaScript

We're going to discuss Turbo in the next tutorial in this series. But in a nutshell, Turbo allows you to turn all of the clicks and form submits on your site into Ajax calls instantly & automatically. And it has several other neat super powers.

Right now, on the cart page, when we remove an item, we did some extra work to submit the delete form via Ajax and reload the cart area also via Ajax. Once you start using Turbo, you absolutely can still do stuff like this... but you'll find that it's less necessary. In this case, if Turbo were active, after confirming in the modal, we could just let the form submit normally... which would automatically happen via Ajax. My point is: Turbo will allow us to have a slick user interface... while writing less custom JavaScript. But if you ever want to do something extra, you are completely free to write custom JavaScript.

Let's Build an Ajax Form Modal!

In that spirit, after talking with a few of you wonderful people, I thought it might be good to show how we could submit a full form via Ajax, including handling validation errors and reloading part of the page after that Ajax call finishes.

So here's the plan: we're going to generate a new product admin section. Then, on the list page. we'll add a "new product" button that, on click opens a modal with a form inside. We'll submit that form via Ajax in the modal, show validation errors in the modal and, finally, on success, reload the product list on the page via another Ajax call. It's going to be an epic example of Stimulus. We're also going to do part of this using jQuery... just in case you prefer using it over vanilla JavaScript.

Upgrading to Bootstrap 5

So let's get going! To start, find your terminal.

The project is currently using Bootstrap version 4. I'm going to upgrade to Bootstrap 5 because I like it's JavaScript components better. Run:

yarn add bootstrap@5 --dev

At this exact moment, Bootstrap 5 is only in beta. So I'll select that version.

Peer Dependency Warnings?

See this "peer dependency" warning? If you ever see these errors - like up here - and they mention Webpack or Babel... it's probably fine. This happens because Encore handles so much stuff for us. These libraries that it's complaining about are installed... but they're installed by Encore directly.

But in this case, this popper thing is going to be a problem... but we can wait to see what error it causes. Bootstrap 5 does change some class names versus Bootstrap 4... but it's minor enough that I'm going to ignore it. If you look at our site, the page still looks just fine. You will notice a few things that don't look right on our form... but we won't worry about fixing those.

Oh, and by the way, if you haven't already after the last chapter, make sure you stop the webpack-bundle-analyzer and restart Encore with:

yarn watch

make:crud

Anyways, now we need to generate our product admin section. Back at your terminal, run

php bin/console make:crud

We want to generate a CRUD for our Product entity. MakerBundle 1.30 now asks you the name of your controller class. We already have a class called ProductController, so let's call this new one ProductAdminController.

And... done! This created the new ProductAdminController, a form class, and a bunch of templates. Go check out the controller: src/Controller/ProductAdminController.php. Oh, let's change the URL to /admin/product... that's probably a better URL.

... lines 1 - 12
/**
* @Route("/admin/product")
*/
class ProductAdminController extends AbstractController
... lines 17 - 95

Let's go see what it looks like! Head over to /admin/product.

And... okay! Good start: this has everything we need... though, it doesn't really fit into our design super well. Let's improve that a tiny bit. Open up this page's template, which is templates/product_admin/index.html.twig. On top, add a <div> class container-fluid and mt-4 for some margin. All the way at the bottom, add the ending div.

... lines 1 - 4
{% block body %}
<div class="container-fluid mt-4">
<h1>Product index</h1>
... lines 8 - 47
</div>
{% endblock %}

Copy this... because we need it in all of our templates. Open edit, do that same thing... add the closing div... new.html.twig... and finally show.html.twig.

When you're done, refresh the list page again. Okay: it's slightly better. It could still use some margin over here, but it's good enough for now.

Adding __toString to Make the Select Field Work

Click to edit a product. Ah!

Error: class Category could not be converted to string.

Rude! This is because the form is trying to make a category select drop down... and it needs to know what text to use for each Category option. Go into src/Entity/Category.php and, anywhere in here - I'll put it at the bottom - add a public function __toString() method that returns $this->name. I'm going to cast that to a string... just in case the name is null.

... lines 1 - 11
class Category
{
... lines 14 - 83
public function __toString(): string
{
return (string) $this->getName();
}
}

Oh, and while we're thinking about the form, I want to make it a bit smaller. In src/Form/ProductType.php, the form contains every field. To make life simpler, remove brand, weight, stockQuantity, imageFilename, and also colors.

... lines 1 - 9
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('description')
->add('price')
->add('category')
;
}
... lines 21 - 27
}

Very nice.

Refresh the edit page now and... it works! If we change something and hit update - yes our buttons do need some styling - that works too.

Oh, but there's one more change I want to make: this will help our example. Back over in ProductAdminController::index(), change the query to sort the newest on top. Do that by changing this findAll() to findBy(), pass it an empty criteria - so it still returns everything - and then sort by id DESC. You could also use a createdAt column if you want.

... lines 1 - 15
class ProductAdminController extends AbstractController
{
... lines 18 - 20
public function index(ProductRepository $productRepository): Response
{
return $this->render('product_admin/index.html.twig', [
'products' => $productRepository->findBy([], ['id' => 'DESC']),
]);
}
... lines 27 - 93
}

Head over and refresh now. Perfect: the highest ids are on top.

Ok: our setup is complete! Next: let's create an "add" button right here on the list page that, on click, opens a Bootstrap modal with our form inside. We'll accomplish that with a Stimulus controller and an Ajax call.

Leave a comment!

6
Login or Register to join the conversation
Macarena I. Avatar
Macarena I. Avatar Macarena I. | posted 2 years ago | edited

Hi! sorry, another little cuestion: I made a CRUD who calls an other CRUD and put it like a treview. In Bootstrap 5 have an exaple of sidebar who collapse the child items. Its work nice but bootstrap work whith "data-bs-target" and need a name-id unique but not numeric. It is posible to make a random alfabetical id in stimulus? or twig? Here is the code: (RANDOMID is the cuestion).

THANKSSSS!!!!


<div  {{ stimulus_controller('arbol')}}>
    {% for objetivo in objetivos %}
        <ul class="list-unstyled ps-0">
            <li class="mb-1">
                <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#RANDOMID-collapse" aria-expanded="true">
                    {{ objetivo.titulo }}
                </button>
                <div class="collapse show" id="RANDOMID-collapse">
                    <ul data-url="{{  path('actividad_index',{'id':objetivo.id})  }}"
                        data-arbol-target="content" class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
                    </ul>
                </div>
            </li>
        </ul>
    {% else %}
        No se encontró registros

    {% endfor %}
Reply

Hi Macarena I.!

Hmm. So first, I'm not sure if this is really a question about Stimulus - or something that should be handled in Stimulus. Because, if I'm understanding things correctly, the "collapse" behavior is not being done by your Stimulus controller, but just by Bootstrap. If I'm correct, then we can simplify the question :). It becomes:

How can I create a unique "id" that can be used in Twig while I loop?

In that case, you can use your objetivo.id for this:


<button   data-bs-target="collapse-{{ objetivo.id }}"  >

<div class="collapse show" id="collapse-{{ objetivo.id }}">

Does this handle the situation for you?

Cheers!

Reply
Macarena I. Avatar

Yess, I was confuse!, I assumed the data-bs-target was stimulus, so it should end with "collapse", it was not a stimulus problem.
Thanks Again!!!!

1 Reply
Macarena I. Avatar

Or maybe there are a better way to do the same?

Reply

How to pass form field 'colors' ?

Reply

Hey Mepcuk

A way to do it would be to pass the input as a "target" to your controller and get the input values from it

Cheers!

Reply
Cat in space

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

This tutorial works perfectly with Stimulus 3!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.18.5
        "symfony/form": "5.2.*", // v5.2.3
        "symfony/framework-bundle": "5.2.*", // v5.2.3
        "symfony/property-access": "5.2.*", // v5.2.3
        "symfony/property-info": "5.2.*", // v5.2.3
        "symfony/proxy-manager-bridge": "5.2.*", // v5.2.3
        "symfony/security-bundle": "5.2.*", // v5.2.3
        "symfony/serializer": "5.2.*", // v5.2.3
        "symfony/twig-bundle": "5.2.*", // v5.2.3
        "symfony/ux-chartjs": "^1.1", // v1.2.0
        "symfony/validator": "5.2.*", // v5.2.3
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.2.1
        "twig/intl-extra": "^3.2", // v3.2.1
        "twig/twig": "^2.12|^3.0" // v3.2.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.2.3
        "symfony/maker-bundle": "^1.27", // v1.30.0
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/stopwatch": "^5.2", // v5.2.3
        "symfony/var-dumper": "^5.2", // v5.2.3
        "symfony/web-profiler-bundle": "^5.2" // v5.2.3
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.13
        "@popperjs/core": "^2.9.1", // 2.9.1
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^5.0.0-beta2", // 5.0.0-beta2
        "core-js": "^3.0.0", // 3.8.3
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.1
        "react-dom": "^17.0.1", // 17.0.1
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "^2.0.1-phylor-6095f2a9", // 2.0.1-phylor-6095f2a9
        "stimulus-use": "^0.24.0-1", // 0.24.0-1
        "sweetalert2": "^10.13.0", // 10.14.0
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.0
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}
userVoice