0

I have what must be a really minor problem that I just can't get my head around. I have a new blog with an index page (showing all articles), category pages, and the article page. It's all on Symfony 5.2.14, with twig etc.

I want to implement a simple 'subscribe now' sign up form (just capturing the visitors email address), which can be re-used on each page - the kind of "Subscribe now to get regular newsletters direct to your inbox" thing. Simple, right?

From a UX viewpoint I don't want to direct the user to complete this form on another stand-alone page, and I want to avoid using any popup modal etc.

I've tried following a few similar questions/answers on here and I cannot get it to work.

  1. I've tried adding a POST method to the same controller/route as the index page - splitting the index page that fetches the articles into a GET method for display and a POST method for the form. It's not working, and comes back with an error that says the 'form' variable isn't valid when I try to 'include' the twig template for the form in the index page template - which suggests to me that basically the POST function/method isn't being called.

  2. I've tried creating a separate controller + form + twig fragment template for the form, and then embedding it into the index page with: {{ render(controller('App\Controller\InsiderSubscribeController::subscribe')) }} it successfully displays the form on the page, but clicking the Subscribe button throws a "No route found for "POST /insider/": Method Not Allowed (Allow: GET)" error. In any case this seems like it wasn't going to work (I just got desperate and threw mud to the wall to see if anything would stick)

  3. I tried following Thomas' approach How to create 2 actions with same path but different http methods Symfony2, but it doesn't work for me (yes, I know that's an old version of Symfony, but I coded it up for s5)

  4. I tried xurshid29's answer here: Multiple Symfony Forms Added Accross Many Pages - and tried to create a twig extension, and still no joy

So here goes on what I have - at this point I have pulled everything back out into separate Controllers/Classes so I can sit back and try to review:

My Index Controller

 /**
 * @Route("/insider/news", name="insider_article_index", methods={"GET"})
 */
public function index(EntityManagerInterface $em, Request $request): Response
{
    $insiderArticles = $this->getDoctrine()->getManager()
        ->getRepository(InsiderArticle::class)
        ->findAll();
    // Get some repository of data, in our case we have an Insider entity
    $insiderRepository = $em->getRepository(InsiderArticle::class);
    // Find all the data on the Insider table, filter our query as we need
    $allInsiderQuery = $insiderRepository->createQueryBuilder('i')
        ->where('i.featured = false')
        ->andWhere('i.promoted = false')
        ->andWhere("i.status LIKE '%Published%'")
        ->leftJoin('i.userpublished', 'au')
        ->getQuery();
    /* @var $paginator \Knp\Component\Pager\Paginator */
    $paginator = $this->paginator;
    // Paginate the results of the query
    $pagination = $paginator->paginate(
    // Doctrine Query, not results
        $allInsiderQuery,
        // Define the page parameter
        $request->query->getInt('page', 1),
        // set the number of items per page
        5
    );
    //dd($pagination);
    // Render the twig view
    return $this->render('insider_article/index.html.twig', [
        'insider_articles' => $insiderArticles,
        'pagination' => $pagination,

    ]);
}

My Subscribe controller

/**
 * @Route("/", name="insider_article_subscribe", methods={"POST"})
 */

public function subscribe(Request $request): Response
{
    $insidersubscriber = new InsiderSubscriber();
    $form = $this->createForm(InsiderSubscribeType::class, $insidersubscriber);

    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        $entityManager = $this->getDoctrine()->getManager();

        $insidersubscriber->setActive(true);
        $insidersubscriber->setDatecreated(new \DateTime());
        $insidersubscriber->setEmailweekly(true);

        $entityManager->persist($insidersubscriber);
        $entityManager->flush();

        return $this->redirectToRoute('insider_article_index'); //doing this for now until I get it working and return the result with Ajax or push a success message to the page

    }
    return $this->render('fragments/insider_public/_subscriber_form.html.twig',
        [
            'insider_subscriber' => $insidersubscriber,
            'form' => $form->createView(),
        ]
    );

}

My Form

 public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('email', EmailType::class, [
            'required' => true,
            'label' => false,
            'attr' => [
                'placeholder' => 'Enter your email address',
            ],
        ])
    ;
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'data_class' => InsiderSubscriber::class,
    ]);
}

My Index page snippet

<div class="p-4">
                {{ render(controller('App\\Controller\\InsiderSubscribeController::subscribe')) }}
                </div>

My twig template for the form render

{{ form_start(form) }}
<h4>Subscribe to Insider</h4>
<p class="mb-4">Get the latest news and updates directly to your inbox with Insider</p>
{{ form_widget(form) }}
<button class="btn btn-wcm">{{ button_label|default('Subscribe') }}</button> {{ form_end(form) }}

There are 2 entities at play here;

  1. The news articles rendered into a list by the Index controller
  2. the subscriber entity where we store our subscribers plus some data about them (preferences, unsubscribe tag, etc)

I haven't implemented sending the subscriber a welcome email yet (I know how to do that) and I suppose I could use Ajax to keep the subscriber on the same page and serve them with a "thanks for signing up" message - but I want to actually get the basic thing working first.

I'm sure the answer must be staring me in the face, but for the life of me right now I cannot get it working.

What am I doing wrong?

yivi
  • 42,438
  • 18
  • 116
  • 138

1 Answers1

-1

I'd do it like this:

First the controller:

<?php

class SubscribeController extends AbstractController
{
    /**
     * @Route("/subscribe-form", name="subscribe")
     */
    public function index(Request $request, EmailService $email)
    {
        $formEntity = new Subscribe();
        $form = $this->createForm(SubscribeType::class, $formEntity);
        
        if ($request->isMethod('POST')) {
            $form->handleRequest($request);

            if ($form->isSubmitted() && $form->isValid()) {
                $formEntity = $form->getData();

                $em = $this->getDoctrine()->getManager();
                
                $em->persist($formEntity);
                $em->flush();

                $email->sendNewSubscriber($formEntity)); // or something you do
                
                $referer = $request->headers->get('referer');
                return $this->redirect($referer); // return to previous page
            }
        }

        // if GET render full page with form
        return $this->render('subscribe/index.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

It can render a page with the form, but really we need only the post stuff of it here.

Then we do the Twig extension to render our form anywhere on the site:

<?php
// src/Twig/SubscribeExtension.php
class SubscribeExtension extends AbstractExtension
{
    private $formFactory;


    public function __construct(FormFactoryInterface $formFactory)
    {
        $this->formFactory = $formFactory;
    }

    public function getFunctions(): array
    {
        return [
            new TwigFunction('renderSubscribeForm', [$this, 'renderSubscribeForm'], [
                'is_safe'           => ['html'],
                'needs_environment' => true,
            ])
        ];
    }

    public function renderSubscribeForm(Environment $environment)
    {
        $formEntity = new Subscribe();
        $form = $this->formFactory->create(SubscribeType::class, $formEntity);

        return $environment->render("utilities/subscribe.html.twig", ['form' => $form->createView()]);
    }
}

Then template utilities/subscribe.html.twig

{{ form_start(form, {'action': path('subscribe'), 'method': 'POST'}) }}
{{ form_row(form.email) }}
{# other form stuff if needed #}
<button type="submit" class="btn btn-primary btn-primary__label">Send</button>
{{ form_end(form) }}

Here we add action to be = our controller path. It is important.

And now we can use {{- renderSubscribeForm() -}} where we want the form to be rendered.

Flash
  • 1,101
  • 1
  • 9
  • 13
  • Дякую. awesome -I had to make a few small tweaks but it works perfectly. I think the 'if ($request->isMethod('POST'))' was about 90% of what I missed - I actually really like Symfony, but the documentation is... patchy. Still not as bad as the horror that is mongodb... – Jerome MacGillivray Aug 14 '21 at 16:35
  • What makes me laugh is that I tried to make this post 'human' by injecting a little humour - including, shock, horror, the idea that failing to solve this problem was driving me to drink... but clearly someone in the editing team decided that was a step to far and edited my post to make it as bland as possible. Nice, I wish I had the same lack of personality as the editor - nice – Jerome MacGillivray Aug 14 '21 at 16:38
  • Нема за що. I'm glad that it helped you. I've seen your question before it was edited and was amused by your knowledge about Horilka. – Flash Aug 16 '21 at 08:57