Symfony custom many-to-many with checkbox - php

Having Session, Person, Participant, I'm trying to create a Session, and a list with all Persons to check the participants.
The PersonInSession table keeps participants_ids and the session_id.
I'm getting the following Error:
Unable to transform value for property path "person": Expected a Doctrine\Common\Collections\Collection object.
I want to create a Session and directly check the participants in the same form.
PersonEntityType
class PersonEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name');
}
}
SessionEntityType
class SessionEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description')
->add('participant', CollectionType::class,
array(
'entry_type' => ParticipantEntityType::class,
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
)
);
}
}
ParticipantEntityType
class ParticipantEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('person', EntityType::class,
array(
'class' => 'AppBundle:PersonEntity',
'choice_label' => 'name',
'expanded' =>'true',
'multiple' => 'true'
)
);
}
}
PersonEntity
class PersonEntity
{
private $id;
private $name;
/**
* #ORM\OneToMany(targetEntity="ParticipantEntity", mappedBy="person")
*/
private $participant;
public function __construct()
{
$this->participant = new ArrayCollection();
}
}
SessionEntity
class SessionEntity
{
private $id;
private $description;
/**
* #ORM\OneToMany(targetEntity="ParticipantEntity", mappedBy="session")
*/
private $participant;
public function __construct()
{
$this->participant = new ArrayCollection();
}
}
ParticipantEntity
class ParticipantEntity
{
private $id;
/**
* #ORM\ManyToOne(targetEntity="PersonEntity", inversedBy="participant")
*/
private $person;
/**
* #ORM\ManyToOne(targetEntity="SessionEntity", inversedBy="participant")
*/
private $session;
public function __construct()
{
$this->person = new ArrayCollection();
}
}

You should have:
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection
class ParticipantEntity
{
// ...
/**
* #var PersonEntity[]|ArrayCollection
*/
private $person;
public function __construct()
{
$this->person = new ArrayCollection();
}
// ...
Otherwise it cannot work as is.

in SessionEntityType, the participant field (you would better rename it to participants as that would be more meaningful) is a CollectionType field. So in its entry_type (ParticipantEntityType) you need to add a field which maps to a Collection property of the target Entity (ParticipantEntity).
In that entity you have person which has wrongly a ManyToOne relationship with your participant. So It should be defined as:
ParticipantEntity
class ParticipantEntity
{
/**
* #ORM\OneToMany(targetEntity="PersonEntity")
*/
private $persons;
public function __construct()
{
$this->person = new ArrayCollection();
}
}
I strongly recommend you to revise your entities, as they are clearly the source of problem.

Related

Build and render a form with inheritance classes in Symfony 6

I have three classes (User, Professionel and Simple, where Professionel and Simple are the children of User). I use inheritance mapping like this:
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\InheritanceType("JOINED")]
#[ORM\DiscriminatorColumn(name:"compte", type: "string")]
#[ORM\DiscriminatorMap(["professionnel"=>Professionel::class, "simple"=> Simple::class])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'string', length: 180, unique: true)]
#[Assert\NotBlank()]
#[Assert\Length(min:2, max:80)]
private $email;
#[ORM\Column(type: 'string', length: 50)]
private $typeCompte;
}
class Professionel extends User
{
private $nameProfessionel;
//getter setter
}
class Simple extends User
{
private $name;
//getter setter
}
I have generated formtype for each entity and extended SimpleType and ProfessionelType like this:
class SimpleType extends UserFormType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Simple::class,
]);
}
}
class ProfessionelType extends UserFormType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('nameProfessionel');
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Professionel::class,
]);
}
}
What I want to do is whenever the user clicks on a radio button (Simple or Professionel), to only show the form that fits.
I have tried to build the form in the controller:
$types = [
'parentForm' => UserFormType::class,
'partForm' => SimpleType::class,
'proForm' => ProfessionelType::class,
];
$forms = [];
foreach ($types as $type1) {
$forms[] = $this->createForm($type1);
}
but I have this error :
Could not load type "App\Form\ChoiceType": class does not exist.

Symfony6 FindBy and entity many to many

I use symfony6 for the first time with php 8.0.17. I want to make a small form that allows me to retrieve "Card" objects. My "Card" objects are bound to many to many color properties. I thought I could retrieve the cards objects via a findBy but I have the following error: Warning: Trying to access array offset on value of type null.
I looked at the documentation but I don't understand where my error comes from.
Cannot use findBy with related object ? Or maybe it's not a good idea to use entity field in form ? As you see the code is simple and it's that i don't understand it's not okay
The code is here :
My Card entity :
<?php
namespace App\Entity;
use App\Repository\CardRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: CardRepository::class)]
class Card
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\ManyToMany(targetEntity: Color::class, inversedBy: 'card')]
private $color;
public function __construct()
{
$this->color = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
/**
* #return Collection<int, Color>
*/
public function getColor(): Collection
{
return $this->color;
}
public function addColor(Color $color): self
{
if (!$this->color->contains($color)) {
$this->color[] = $color;
}
return $this;
}
public function removeColor(Color $color): self
{
$this->color->removeElement($color);
return $this;
}
}
My Color entity :
<?php
namespace App\Entity;
use App\Repository\ColorRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ColorRepository::class)]
class Color
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'string', length: 255)]
private $name;
#[ORM\ManyToMany(targetEntity: Card::class, mappedBy: 'color')]
private $card;
public function __construct()
{
$this->card = 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;
}
public function __toString()
{
return $this->getName();
}
/**
* #return Collection<int, Card>
*/
public function getCard(): Collection
{
return $this->card;
}
public function addCard(Cards $card): self
{
if (!$this->card->contains($card)) {
$this->card[] = $card;
$card->addColor($this);
}
return $this;
}
public function removeCard(Cards $card): self
{
if ($this->cards->removeElement($card)) {
$card->removeColor($this);
}
return $this;
}
}
My Search Form :
<?php
namespace App\Form;
use App\Entity\Color;
use App\Entity\Keyword;
use App\Entity\Type;
use App\Form\DataTransformer\ColorToStringTransformer;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
class CardSearchType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['required' => false])
->add('color', EntityType::class, [
'class' => Color::class,
// uses the Cards.name property as the visible option string
'choice_label' => 'name',
// used to render a select box, check boxes or radios
// 'multiple' => true,
// 'expanded' => true,
// To add little test for empty value
'placeholder' => 'Choose a color.',
'required' => false,
])
->add('save', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
}
}
And to finish the controller action :
class DeckBuilderController extends AbstractController
{
#[Route('/deck/builder', name: 'app_deck_builder')]
public function index(Request $request, CardRepository $cardRepository): Response
{
$form = $this->createForm(CardSearchType::class);
$form->handleRequest($request);
$cards = null;
if ($form->isSubmitted() && $form->isValid())
{
// $form->getData() holds the submitted values
$cardData = $form->getData();
$cards = $cardRepository->findBy($cardData); => error Warning: Trying to access array offset on value of type null
}
return $this->render('deck_builder/index.html.twig', [
'controller_name' => 'DeckBuilderController',
'form' => $form->createView(),
'cards' => $cards,
]);
}
Thank in advance for your response.
$form->getData() doesn't return an array but an object with the form content.
My guess is that you want to use $form->get('color')->getData() instead
Correcting your code, I would try something like this :
public function index(Request $request, CardRepository $cardRepository): Response {
$cards = array();
$form = $this->createForm(CardSearchType::class);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$cards = $cardRepository->findBy(array('color'=>$form->get('color')->getData()));
}
return $this->renderForm('deck_builder/index.html.twig', array(
'controller_name' => 'DeckBuilderController',
'form' => $form,
'cards' => $cards,
));
}
To solve my problem I finally created a query by hand and made the join in DQL.

Form rendering for many to many relation in symfony 4 does not work

I have many to many relation between two entities Recipient and Recipient Group. I am trying to setup a form for Recipient where I must be able to add multiple Recipient Group to Recipient. But when I try to run the rendered form, I get the following error even though addXXX and removeXXX methods exist in both the classes.
Could not determine access type for property "recipientGroups" in class
"App\Entity\Recipient": The property "recipientGroups" in class "App\Entity\Recipient" can
be defined with the methods "addRecipientGroup()", "removeRecipientGroup()" but the new
value must be an array or an instance of \Traversable, "App\Entity\RecipientGroup" given.
Entity/Recipient
**
*
#ORM\Entity(repositoryClass="App\Repository\RecipientRepository")
*/
class Recipient
{
...
/**
* #ORM\ManyToMany(targetEntity="App\Entity\RecipientGroup", inversedBy="recipients")
*/
private $recipientGroups;
public function __construct()
{
$this->recipientGroups = new ArrayCollection();
}
/**
* #return Collection|RecipientGroup[]
*/
public function getRecipientGroups(): Collection
{
return $this->recipientGroups;
}
public function addRecipientGroup(RecipientGroup $recipientGroup): self
{
if (!$this->recipientGroups->contains($recipientGroup)) {
$this->recipientGroups[] = $recipientGroup;
$recipientGroup->addRecipient($this);
}
return $this;
}
public function removeRecipientGroup(RecipientGroup $recipientGroup): self
{
if ($this->recipientGroups->contains($recipientGroup)) {
$this->recipientGroups->removeElement($recipientGroup);
$recipientGroup->removeRecipient($this);
}
return $this;
}
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
public function setCreatedAt(DateTime $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): DateTime
{
return $this->updatedAt;
}
public function setUpdatedAt(DateTime $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
}
Entity/RecipientGroup:
/**
* #ORM\Entity(repositoryClass="App\Repository\RecipientGroupRepository")
*/
class RecipientGroup
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Recipient", mappedBy="recipientGroups")
*/
private $recipients;
public function __construct()
{
$this->recipients = new ArrayCollection();
}
* #return Collection|Recipient[]
*/
public function getRecipients(): Collection
{
return $this->recipients;
}
public function addRecipient(Recipient $recipient): self
{
if (!$this->recipients->contains($recipient)) {
$this->recipients[] = $recipient;
$recipient->addRecipientGroup($this);
}
return $this;
}
public function removeRecipient(Recipient $recipient): self
{
if ($this->recipients->contains($recipient)) {
$this->recipients->removeElement($recipient);
$recipient->removeRecipientGroup($this);
}
return $this;
}
}
form/RecipientType
class RecipientType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('fullName')
->add('email')
->add('recipientGroups', CollectionType::class, [
'entry_type' => RecipientGroupBlockType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'help' => '<a data-collection="add" class="btn btn-info btn-sm" href="#">Add Recipient Group</a>',
'help_html' => true,
'by_reference' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Recipient::class,
]);
}
}
form/RecipientGroupBlockType:
<?php
namespace App\Form;
use App\Entity\Recipient;
use App\Entity\RecipientGroup;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class RecipientGroupBlockType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('recipientGroups', EntityType::class,[
'class' => RecipientGroup::class,
'choice_label' => 'title',
'placeholder' => 'Select an option'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Recipient::class,
]);
}
}

Symfony - form - many to many relation - collection type

Here is the situation:
I have an entity Property
class Property
{
/**
* #ORM\Id
* #ORM\Column(type="string")
* #ORM\GeneratedValue(strategy="UUID")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="PropertyEquipment", inversedBy="properties")
*/
protected $propertyEquipments;
public function __construct()
{
$this->propertyEquipments = new ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function addPropertyEquipment(\AppBundle\Entity\PropertyEquipment $propertyEquipment)
{
$this->propertyEquipments[] = $propertyEquipment;
return $this;
}
public function removePropertyEquipment(\AppBundle\Entity\PropertyEquipment $propertyEquipment)
{
$this->propertyEquipments->removeElement($propertyEquipment);
}
public function getPropertyEquipments()
{
return $this->propertyEquipments;
}
}
And the entity PropertyEquipment:
class PropertyEquipment
{
/**
* #ORM\Id
* #ORM\Column(type="string")
* #ORM\GeneratedValue(strategy="UUID")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="Property", mappedBy="propertyEquipments")
*/
protected $properties;
/**
* #ORM\Column(type="string", length=100)
* #Gedmo\Translatable
*/
protected $equipmentName;
public function __construct()
{
$this->properties = new ArrayCollection();
}
/**
* Get id
*
* #return string
*/
public function getId()
{
return $this->id;
}
/**
* #return mixed
*/
public function getEquipmentName()
{
return $this->equipmentName;
}
/**
* #param mixed $equipmentName
*/
public function setEquipmentName($equipmentName)
{
$this->equipmentName = $equipmentName;
}
public function addProperty(Property $property)
{
$this->properties[] = $property;
return $this;
}
public function removeProperty(Property $property)
{
$this->properties->removeElement($property);
}
public function getProperties()
{
return $this->properties;
}
}
The form PropertyCreation
class PropertyCreation extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//with this I see the values coming from DB in the template
->add("propertyEquipments", PropertyEquipmentCreation::class)
//with this it's empty :/
/*->add("propertyEquipments", CollectionType::class, array(
"entry_type" => PropertyEquipmentCreation::class,
))*/
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Property::class
));
}
}
Here is the form PropertyEquipmentCreation:
class PropertyEquipmentCreation extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('propertyEquipment', EntityType::class, [
'class' => 'AppBundle\Entity\PropertyEquipment',
'choice_label' => 'equipmentName',
'expanded' => true,
'multiple' => true
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => PropertyEquipment::class,
]);
}
}
And the controller
public function createPropertyAction(Request $request)
{
$property = new Property();
$form = $this->createForm(PropertyCreation::class, $property);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($property);
$entityManager->flush();
return $this->redirectToRoute('homepage');
}
return $this->render('form/owner_create_property.html.twig', ["form" => $form->createView()]);
}
My error:
Expected value of type "Doctrine\Common\Collections\Collection|array" for association field "AppBundle\Entity\Property#$propertyEquipments", got "Doctrine\Common\Collections\ArrayCollection" instead.
Must I transform these with something like class PropertyEquipmentTransformer implements DataTransformerInterface?
I think you should use getParent() function in PropertyEquipmentCreation and inherit from EntityType::class then put all your field configs in the configureOptions() function (remove the buildForm function) and it should work.
You are having this problem because it is a compound form in your implementation and no simple form and symfony is unable to resolve which field created inside the subform needs to be used as source for the entity field
First !
Big thanks to Nickolaus !
Here is the solution (PropertyEquipmentCreation):
namespace AppBundle\Form\Type;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PropertyEquipmentCreation extends AbstractType
{
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => 'AppBundle\Entity\PropertyEquipment',
'choice_label' => 'equipmentName',
'expanded' => true,
'multiple' => true,
]);
}
public function getParent()
{
return EntityType::class;
}
}
And for (PropertyCreation)
<?php
namespace AppBundle\Form;
use AppBundle\Form\Type\PropertyEquipmentCreation;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PropertyCreation extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description', TextareaType::class)
->add('name', TextType::class)
->add("propertyEquipments", PropertyEquipmentCreation::class)
->add('save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Property::class
));
}
}
Many thanks !
Try using Doctrine ArrayCollection instead of ArrayCollections:
$this->propertyEquipments = new \Doctrine\Common\Collections\ArrayCollection();
This should work!

Form embedded for many to many & many to one in symfony2.

I have a problem when I try to embed a form in Symfony2 for many to many relations or many to one relations.
I have two entities called 'Address' and 'AddressType' and they are related as you can see on code below.
What I tried to do is when I created a form for Address, I embedded the form for AddressType. I've already tried embedding a collection of AddressType to Address form, but when I try to embed the result of this to Address it seems not to work.
Address Entity
namespace Webmuch\ProductBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
*/
class Address
{
protected $id;
protected $line1;
protected $line2;
protected $state;
protected $city;
protected $zip;
protected $country;
protected $phone;
/**
* #ORM\ManyToOne(targetEntity="AddressType")
* #ORM\JoinColumn(name="address_type_id", referencedColumnName="id")
*/
protected $type;
public function __construct()
{
$this->type = new ArrayCollection();
}
/**
* Set type
*
* #param Webmuch\ProductBundle\Entity\AddressType $type
*/
public function setType(\Webmuch\ProductBundle\Entity\AddressType $type)
{
$this->type = $type;
}
/**
* Get type
*
* #return Webmuch\ProductBundle\Entity\AddressType
*/
public function getType()
{
return $this->type;
}
}
AddressType Entity:
namespace Webmuch\ProductBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class AddressType
{
protected $id;
protected $title;
public function __construct()
{
$this->title = false;
}
}
In form section->
form
AddressType:
namespace Webmuch\AdminBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class AddressType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('line1')
->add('line2')
->add('city')
->add('zip')
->add('country')
->add('phone')
->add('type','collection', array( 'type' => new AddressTypeType(),
'allow_add' => true,
'prototype' => true,
'by_reference' => false,
));
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'Webmuch\ProductBundle\Entity\Address');
}
public function getName()
{
return 'address';
}
}
AddressTypeType:
namespace Webmuch\AdminBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class AddressTypeType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('title');
;
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Webmuch\ProductBundle\Entity\AddressType',
);
}
public function getName()
{
return 'addresstypetype';
}
}
Controller Section->
namespace Webmuch\AdminBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Webmuch\ProductBundle\Entity\Address;
use Webmuch\AdminBundle\Form\AddressType;
/**
* Address controller.
*
* #Route("/cusadmin/address")
*/
class AddressController extends Controller
{
/**
* Displays a form to create a new Address entity.
*
* #Route("/new", name="admin_address_new")
* #Template()
*/
public function newAction()
{
$entity = new Address();
$form = $this->createForm(new AddressType(), $entity);
return array(
'entity' => $entity,
'form' => $form->createView()
);
}
}
I've spent the whole day stuck with this and I have tried a lot of things but I couldn't manage get it working.
Any help is appreciated!
Thanks
Edit form AddressType: nd write this code,may be this is help full....
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('line1')
->add('line2')
->add('city')
->add('zip')
->add('country')
->add('phone')
->add('type','entity', array('class'=>'WebmuchProductBundle:AddressType','property'=>'value','multiple'=>true
));

Categories