Search code examples
phpsymfonycontrollerargumentsentity

Symfony 7 - argument that could not be resolved


I'm a beginner and I'm trying to follow a tutorial to create a blog under Symfony 7, unfortunately I find myself stuck with an error when I want to display the page of an article using his address (slug) I get:

Controller "App\Controller\ArticleController::show" requires the "$article" argument that could not be resolved. Cannot autowire argument $article of "App\Controller\ArticleController::show()": it needs an instance of "App\Entity\Article" but this type has been excluded in "config/services.yaml".

It was when I injected my Article entity into the show function of my ArticleController that the problem occurred.

<?php

namespace App\Controller;

use App\Entity\Article;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class ArticleController extends AbstractController
{
    #[Route('/article/{slug}', name: 'article_show')]
    public function show(Article $article): Response
    {     
        return $this->render('article/show.html.twig', [
            'article' => $article,
        ]);
    }
}

When I use the link from the home page, the route displayed is correct (with the slug).

Any idea how I could fix this? (it must be something really simple but I don't understand)

I tried to check my code, especially the entity:

<?php

namespace App\Entity;

use App\Model\TimestampedInterface;
use App\Repository\ArticleRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article implements TimestampedInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(length: 255)]
    private ?string $slug = null;

    #[ORM\Column(type: Types::TEXT, nullable: true)]
    private ?string $content = null;

    #[ORM\Column(length: 100, nullable: true)]
    private ?string $featuredText = null;

    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
    private ?\DateTimeInterface $createdAt = null;

    #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
    private ?\DateTimeInterface $updatedAt = null;

    /**
     * @var Collection<int, Category>
     */
    #[ORM\ManyToMany(targetEntity: Category::class, mappedBy: 'articles')]
    private Collection $categories;

    /**
     * @var Collection<int, Comment>
     */
    #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article', orphanRemoval: true)]
    private Collection $comments;

    #[ORM\ManyToOne]
    private ?Media $featuredImage = null;

    public function __construct()
    {
        $this->categories = new ArrayCollection();
        $this->comments = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): static
    {
        $this->title = $title;

        return $this;
    }

    public function getSlug(): ?string
    {
        return $this->slug;
    }

    public function setSlug(string $slug): static
    {
        $this->slug = $slug;

        return $this;
    }

    public function getContent(): ?string
    {
        return $this->content;
    }

    public function setContent(?string $content): static
    {
        $this->content = $content;

        return $this;
    }

    public function getFeaturedText(): ?string
    {
        return $this->featuredText;
    }

    public function setFeaturedText(?string $featuredText): static
    {
        $this->featuredText = $featuredText;

        return $this;
    }

    public function getCreatedAt(): ?\DateTimeInterface
    {
        return $this->createdAt;
    }

    public function setCreatedAt(\DateTimeInterface $createdAt): static
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    public function getUpdatedAt(): ?\DateTimeInterface
    {
        return $this->updatedAt;
    }

    public function setUpdatedAt(?\DateTimeInterface $updatedAt): static
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    /**
     * @return Collection<int, Category>
     */
    public function getCategories(): Collection
    {
        return $this->categories;
    }

    public function addCategory(Category $category): static
    {
        if (!$this->categories->contains($category)) {
            $this->categories->add($category);
            $category->addArticle($this);
        }

        return $this;
    }

    public function removeCategory(Category $category): static
    {
        if ($this->categories->removeElement($category)) {
            $category->removeArticle($this);
        }

        return $this;
    }

    /**
     * @return Collection<int, Comment>
     */
    public function getComments(): Collection
    {
        return $this->comments;
    }

    public function addComment(Comment $comment): static
    {
        if (!$this->comments->contains($comment)) {
            $this->comments->add($comment);
            $comment->setArticle($this);
        }

        return $this;
    }

    public function removeComment(Comment $comment): static
    {
        if ($this->comments->removeElement($comment)) {
            // set the owning side to null (unless already changed)
            if ($comment->getArticle() === $this) {
                $comment->setArticle(null);
            }
        }

        return $this;
    }

    public function getFeaturedImage(): ?Media
    {
        return $this->featuredImage;
    }

    public function setFeaturedImage(?Media $featuredImage): static
    {
        $this->featuredImage = $featuredImage;

        return $this;
    }
}

You understood it, the goal is therefore to display the article page :

{% extends 'base.html.twig' %}

{% block title %}{{ article.title }}{% endblock %}

{% block body %}
    <div class="container">
        <h1>{{ article.title }}</h1>
        <hr>
        {{ article.content|raw }}
    </div>
{% endblock %}

Many thanks,

EDIT1: Here is the code of the home page where the link to the article page is generated. But whatever the slug indicated in the url (whether it exists or not in the database), the error message is the same.

{% extends 'base.html.twig' %}

{% block title %}Accueil{% endblock %}

{% block body %}
        <main class="container">
            <div class="row">
                <div class="col-md-8">
                    <h3 class="pb-4 mb-4 fst-italic border-bottom">Articles récents</h3>
                    <div id="articles-list">
                        {% include 'article/list.html.twig' with { articles } %}
                    </div>
                </div>
                <div class="col-md-4">
                    <div class="position-sticky" style="top: 2rem;">
                        {% include 'widget/about.html.twig' %}
                        {% include 'widget/categories.html.twig' %}
                    </div>
                </div>
            </div>
        </main>
{% endblock %}

The template list.html.twig :

{% for article in articles %}
    {% include 'article/item.html.twig' with { article } %}
{% endfor %}

item.html.twig :

{% set article_show = path('article_show', { 'slug': article.slug }) %}

<article class="mb-5">
    <div class="row">
        <div class="col-md-5">
            {% if article.featuredImage %}
                    <a href="{{ article_show }}">
                        <img src="/uploads/{{ article.featuredImage.filename }}" alt="{{ article.featuredImage.altText }}" loading="lazy" width="350" height="205">
                    </a>
            {% endif %}
        </div>
        <div class="col-md-7">
            <h2>
                <a class="text-decoration-none" href="{{ article_show }}">{{ article.title }}</a>
            </h2>
            <p>
                {{ article.createdAt|date('d M Y') }}
            </p>
            {{ article.featuredText ?: article.content|striptags|slice(0, 130) ~ '...' }}
        </div>
    </div>
</article>

EDIT2, I only have one article in the database and it does have a slug. Normally the slug cannot be null, but if I manage to get past this step, I will add a redirection in case the article cannot be found:

class ArticleController extends AbstractController
{
    #[Route('/article/{slug}', name: 'article_show')]
    public function show(?Article $article): Response
    {
        if (!$article) {
            return $this->redirectToRoute('app_home');
        }
        
        return $this->render('article/show.html.twig', [
            'article' => $article,
        ]);
    }
}

For the moment if I apply this code, the article is not found and we return to the home page (this seems normal considering the error I had before that).

EDIT3, Here is the HomeController with the findAll():

<?php

namespace App\Controller;

use App\Repository\ArticleRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class HomeController extends AbstractController
{
    #[Route('/', name: 'app_home')]
    public function index(ArticleRepository $articleRepo): Response
    {
        return $this->render('home/index.html.twig', [
            'articles' => $articleRepo->findAll()
        ]);
    }
}

And ArticleRepository :

<?php

namespace App\Repository;

use App\Entity\Article;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class ArticleRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Article::class);
    }
}

EDIT4, I just tested by replacing the slug with the article id in ArticleController:

<?php
  
namespace App\Controller;
  
use App\Entity\Article;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
  
class ArticleController extends AbstractController
{
    #[Route('/article/{id}', name: 'article_show')]
    public function show(Article $article): Response
    {
        return $this->render('article/show.html.twig', [
            'article' => $article,
        ]);
    }
}

This works perfectly using the URL containing the article id, which makes me think my code is good. However, still no solution to make it work with the slug.


Solution

  • After hours of struggling and exploring, I found a simple solution, just set the value "true" for "auto_mapping" in the doctrine.yaml file:

    orm:
            auto_generate_proxy_classes: true
            enable_lazy_ghost_objects: true
            report_fields_where_declared: true
            validate_xml_mapping: true
            naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
            auto_mapping: true