php – The current field `category` is not linked to an admin. Please create one for the target model: “ when using Form Types other than ModelType, Symfony

I’m trying to create/expand upon a basic app following the Symfony Sonata Admin Bundle documentation. There are BlogPost, Category, and Author entities and their respective admins. BlogPost has a ManyToOne relationship to both Category and Author.

I notice from the start that I had trouble applying different Form Types to ‘Category’ but thought maybe I was missing something. After adding ‘Author,’ running migrations, etc. I found that I was able to add form types other than ModelType, such as ModelAutocompleteType or ModelListType to my author listing on BlogPostAdmin’s configureFormFields without any issue. However when I tried to do the same to Category I’d get the error:

The current field `category` is not linked to an admin. Please create one for the target model: ``.

I have dropped and recreated the database and caches, even created a new MyCategory entity in case something was wrong when Category was generated. (Provided in examples)

I cannot seem to figure out why I can add different form types to Author but not Category.

Entities:

Entity/BlogPost.php

<?php

namespace AppEntity;

use AppRepositoryBlogPostRepository;
use DoctrineORMMapping as ORM;

#[ORMEntity(repositoryClass: BlogPostRepository::class)]
class BlogPost
{
    #[ORMId]
    #[ORMGeneratedValue]
    #[ORMColumn(type: 'integer')]
    private $id;

    #[ORMColumn(type: 'string', length: 255)]
    private $title;

    #[ORMColumn(type: 'boolean',  nullable: true)]
    private $draft;

    #[ORMColumn(type: 'string', length: 255, nullable: true)]
    private $body;


    #[ORMManyToOne(targetEntity: Author::class, inversedBy: 'posts')]
    private $author;

    #[ORMManyToOne(targetEntity: MyCategory::class, inversedBy: 'posts')]
    private $myCategory;

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

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

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

        return $this;
    }

    public function isDraft(): ?bool
    {
        return $this->draft;
    }

    public function setDraft(bool $draft): self
    {
        $this->draft = $draft;

        return $this;
    }

    public function getBody(): ?string
    {
        return $this->body;
    }

    public function setBody(?string $body): self
    {
        $this->body = $body;

        return $this;
    }



    public function getAuthor(): ?Author
    {
        return $this->author;
    }

    public function setAuthor(?Author $author): self
    {
        $this->author = $author;

        return $this;
    }

    public function getMyCategory(): ?MyCategory
    {
        return $this->myCategory;
    }

    public function setMyCategory(?MyCategory $myCategory): self
    {
        $this->myCategory = $myCategory;

        return $this;
    }
}

Entity/Author.php

<?php

namespace AppEntity;

use AppRepositoryAuthorRepository;
use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
use DoctrineORMMapping as ORM;

#[ORMEntity(repositoryClass: AuthorRepository::class)]
class Author
{
    #[ORMId]
    #[ORMGeneratedValue]
    #[ORMColumn(type: 'integer')]
    private $id;

    #[ORMColumn(type: 'string', length: 255)]
    private $name;

    #[ORMOneToMany(mappedBy: 'author', targetEntity: BlogPost::class)]
    private $posts;

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

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

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    /**
     * @return Collection<int, BlogPost>
     */
    public function getPosts(): Collection
    {
        return $this->posts;
    }

    public function addPost(BlogPost $post): self
    {
        if (!$this->posts->contains($post)) {
            $this->posts[] = $post;
            $post->setAuthor($this);
        }

        return $this;
    }

    public function removePost(BlogPost $post): self
    {
        if ($this->posts->removeElement($post)) {
            // set the owning side to null (unless already changed)
            if ($post->getAuthor() === $this) {
                $post->setAuthor(null);
            }
        }

        return $this;
    }

    public function __toString(): string
    {
        return $this->getName();
    }
}

Entity/MyCategory.php

<?php

namespace AppEntity;

use AppRepositoryMyCategoryRepository;
use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
use DoctrineORMMapping as ORM;

#[ORMEntity(repositoryClass: MyCategoryRepository::class)]
class MyCategory
{
    #[ORMId]
    #[ORMGeneratedValue]
    #[ORMColumn(type: 'integer')]
    private $id;

    #[ORMColumn(type: 'string', length: 255)]
    private $name;

    #[ORMOneToMany(mappedBy: 'myCategory', targetEntity: BlogPost::class)]
    private $posts;

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

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

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    /**
     * @return Collection<int, BlogPost>
     */
    public function getPosts(): Collection
    {
        return $this->posts;
    }

    public function addPost(BlogPost $post): self
    {
        if (!$this->posts->contains($post)) {
            $this->posts[] = $post;
            $post->setMyCategory($this);
        }

        return $this;
    }

    public function removePost(BlogPost $post): self
    {
        if ($this->posts->removeElement($post)) {
            // set the owning side to null (unless already changed)
            if ($post->getMyCategory() === $this) {
                $post->setMyCategory(null);
            }
        }

        return $this;
    }
}

Admins:

Admin/BlogPostAdmin.php

<?php

namespace AppAdmin;

use SonataAdminBundleAdminAbstractAdmin;
use SonataAdminBundleDataGridListMapper;
use SonataAdminBundleFormFormMapper;
use SonataAdminBundleShowShowMapper;
use SymfonyComponentFormExtensionCoreTypeTextType;
use SymfonyComponentFormExtensionCoreTypeTextareaType;
use AppEntityMyCategory;
use AppEntityAuthor;
use SymfonyBridgeDoctrineFormTypeEntityType;
use SonataAdminBundleDatagridDatagridMapper;
use SonataAdminBundleFormTypeModelType;
use SonataAdminBundleFormTypeModelListType;
use AppEntityBlogPost;
use SonataAdminBundleFormTypeModelAutocompleteType;

final class BlogPostAdmin extends AbstractAdmin
{
    protected function configureFormFields(FormMapper $form): void
    {
        $form
            ->tab('Post')
            ->with("Content", ['class' => 'col-md-9'])
            ->add('title', TextType::class)
            ->add('body', TextareaType::class)
            ->end()
            ->with('Meta data', ['class' => 'col-md-3'])
            ->add('mycategory', ModelAutocompleteType::class, ['property' => 'name']) // Causing the error
            ->add('author', ModelAutocompleteType::class, ['property' => 'name'])
            ->add('draft')
            ->end()
            ->end()
            ->tab('Publish Options')
            // ...
            ->end();
    }
    protected function configureListFields(ListMapper $list): void
    {
        $list
            ->addIdentifier('title')
            ->add('author.name')
            ->add('draft');
    }

    protected function configureDatagridFilters(DatagridMapper $datagrid): void
    {
        $datagrid
            ->add('title')
            ->add('author', null, [
                'field_type' => EntityType::class,
                'field_options' => [
                    'class' => Author::class,
                    'choice_label' => 'name',
                ],
            ])

        ;
    }

    protected function configureShowFields(ShowMapper $show): void
    {
        $show
            ->tab('Post')
            ->with('Content', ['class' => 'col-md-9'])
            ->add('id')
            ->add('title')
            ->add('body')
            ->add('author.name')


            ->end()
            ->with('Meta data', ['class' => 'col-md-3'])
            // ->add('category.name')
            ->end()
            ->end()

    }

    public function toString(object $object): string
    {
        return $object instanceof BlogPost ? $object->getTitle() : 'Blog Post'; // Shown in the breadcrumb on the create view
    }
}

Admin/AuthorAdmin.php

<?php


namespace AppAdmin;

use AppEntityAuthor;
use SonataAdminBundleAdminAbstractAdmin;
use SonataAdminBundleDatagridDatagridMapper;
use SonataAdminBundleDatagridListMapper;
use SonataAdminBundleFormFormMapper;
use SonataAdminBundleShowShowMapper;
use SymfonyComponentFormExtensionCoreTypeTextType;

final class AuthorAdmin extends AbstractAdmin
{
    protected function configureFormFields(FormMapper $form): void
    {
        $form->add('name', TextType::class);
    }

    protected function configureDatagridFilters(DatagridMapper $datagrid): void
    {
        $datagrid->add('name');
    }

    protected function configureListFields(ListMapper $list): void
    {
        $list->addIdentifier('name');
    }

    protected function configureShowFields(ShowMapper $show): void
    {
        $show->add('name');
    }

    public function toString(object $object): string
    {
        return $object instanceof Author ? $object->getName() : 'Author'; // Shown in the breadcrumb on the create view
    }
}

Admin/CategoryAdmin.php

<?php


namespace AppAdmin;

use AppEntityMyCategory;
use SonataAdminBundleAdminAbstractAdmin;
use SonataAdminBundleDatagridDatagridMapper;
use SonataAdminBundleDatagridListMapper;
use SonataAdminBundleFormFormMapper;
use SonataAdminBundleShowShowMapper;
use SymfonyComponentFormExtensionCoreTypeTextType;



final class MyCategoryAdmin extends AbstractAdmin
{
    protected function configureFormFields(FormMapper $form): void
    {
        $form->add('name', TextType::class);
    }

    protected function configureDatagridFilters(DatagridMapper $datagrid): void
    {
        $datagrid->add('name');
    }

    protected function configureListFields(ListMapper $list): void
    {
        $list->addIdentifier('name');
    }

    protected function configureShowFields(ShowMapper $show): void
    {
        $show->add('name');
    }

    public function toString(object $object): string
    {
        return $object instanceof MyCategory ? $object->getName() : 'Category'; // Shown in the breadcrumb on the create view
    }
}

Config:

config/services.yaml

parameters:
  locale: "en"

services:
  # default configuration for services in *this* file
  _defaults:
    autowire: true # Automatically injects dependencies in your services.
    autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
  admin.category:
    class: AppAdminMyCategoryAdmin
    tags:
      - {
          name: sonata.admin,
          model_class: AppEntityMyCategory,
          manager_type: orm,
          label: Category,
        }
  admin.blog_post:
    class: AppAdminBlogPostAdmin
    tags:
      - {
          name: sonata.admin,
          model_class: AppEntityBlogPost,
          manager_type: orm,
          label: "Blog post",
        }
  admin.author:
    class: AppAdminAuthorAdmin
    tags:
      - {
          name: sonata.admin,
          model_class: AppEntityAuthor,
          manager_type: orm,
          label: "Author",
        }

  # makes classes in src/ available to be used as services
  # this creates a service per class whose id is the fully-qualified class name
  App:
    resource: "../src/"
    exclude:
      - "../src/DependencyInjection/"
      - "../src/Entity/"
      - "../src/Kernel.php"

Leave a Comment