How to configure entities in zf2 for ManyToOne unidirectional doctrine mapping - php

I'm having trouble developing a form in zf2 using a doctrine ManyToOne unidirectional relationship. My entities look like this:
namespace AdminMyPages\Entity;
class MyPageItem
{
// ...
/**
* #ORM\ManyToOne(targetEntity="MyMessage")
* #ORM\JoinColumn(name="myMessageID", referencedColumnName="myMessageID")
**/
private $myMessage;
// ...
/**
* Allow null to remove association
*
* #param Collection $myMessage
*/
public function setMyMessage(Collection $myMessage = null)
{
$this->myMessage = $myMessage;
}
/**
* #return myMessage
*/
public function getMyMessage()
{
return $this->myMessage;
}
}
class MyMessage
{
// ...
}
The fieldset for MyPageItemFieldset looks like this:
namespace AdminMyPages\Form;
class MyPageItemFieldset extends Fieldset implements InputFilterProviderInterface
{
public function __construct(ObjectManager $objectManager)
{
parent::__construct('mypage-item-fieldset');
$this->setHydrator(new DoctrineHydrator($objectManager, 'AdminMyPages\Entity\MyPageItem'))
->setObject(new MyPageItem());
// ...
$myMessageFieldset = new MyMessageFieldset($objectManager);
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'myMessage',
'options' => array(
'count' => 1,
'target_element' => $myMessageFieldset
)
));
}
public function getInputFilterSpecification()
{
// ...
return array(
'myMessage' => array(
'required' => false
),
);
}
}
With this configuration I am able to "get" data from the MyMessage through getMyMessage(), so I know that the tables have been joined. However, when I try to bind the entity in a form, I get an error:
File:
C:\xampp\htdocs\GetOut\vendor\zendframework\zendframework\library\Zend\Form\Element\Collection.php:167
Message:
Zend\Form\Element\Collection::setObject expects an array or Traversable object argument; received "DoctrineORMModule\Proxy\__CG__\AdminMyPages\Entity\MyMessage"
Stack trace:
#0 ... Zend\Form\Element\Collection->setObject(Object(DoctrineORMModule\Proxy\__CG__\AdminMyPages\Entity\MyMessage))
...
One thought I have is that, since the ManyToOne relationship will only produce a single match, the MyMessage fieldset is hardly a collection - it's just one item - so Zend\Form\Element\Collection might not be the right form element to use. But, if it's not a collection, what is it?

You are totally right. Your MyMessage should not be a Collection. It should simply be an instance of MyMessage.
You defined a ManyToOne between MyPageItem and MyMessage meaning one page item has one message and a message has many page items. But since it is a unidirectional relationship that last part is never defined.
So the setter should look like this:
/**
* #param MyMessage $myMessage
*/
public function setMyMessage(MyMessage $myMessage = null)
{
$this->myMessage = $myMessage;
}
And you should also change your form field definition to a single MyMessage item.

Related

How do I create a hasMany relationship with non-standard foreign keys

I have a Laravel 8 application where a User hasMany Notifications. The notifications table has two required keys: sender_id and recipient_id. These are both instances of User. Here's how I setup the relationship in the model:
App\Models\User
public function recievedNotifications()
{
return $this->hasMany(Notification::class, 'recipient_id');
}
public function sentNotifications()
{
return $this->hasMany(Notification::class, 'sender_id');
}
App\Models\Notification
public function recipient()
{
return $this->belongsTo(User::class);
}
public function sender()
{
return $this->belongsTo(User::class);
}
I want to create two users and properly associate a Notification instance for those two users. I can't quite get the syntax.
I wanted to try something like this but it assigns the incorrect user ids when I manually look at the database entries:
$sender = User::factory()->create();
$recipient = User::factory()->create();
$notification = App\Models\Notification::factory()->hasSender($sender)->hasRecipient($recipient)->create();
I am not certain if this is a function of:
1: Not having the relationships set up in the PHP Models
2: Not creating the factory the correct way
The Notification factory is defined as the following:
<?php
namespace Database\Factories;
use App\Models\Notification;
use Illuminate\Database\Eloquent\Factories\Factory;
class NotificationFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Notification::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
//
'message' => $this->faker->sentence,
'is_read' => rand(0, 1),
];
}
}
How do I properly create Notifications with a recipient and sender using factories?
Why just not use it like this, by assigning the ids from the factory?
$sender = User::factory()->create();
$recipient = User::factory()->create();
$notification = App\Models\Notification::factory()->create([
'recipient_id' => $recipient->id,
'sender_id' => $sender->id
]);
The relationships look good, so I'm leaning towards not using the factory properly. Factories simply provide default values to use when instantiating new models if you don't provide the attributes explicitly.
You can set these values by supplying an array in the create() or make() method:
$sender = User::factory()->create();
$recipient = User::factory()->create();
$notification = Notification::factory()->create([
'sender_id' => $sender->getKey(),
'recipient_id' => $recipient->getKey(),
]);

filter entity fields on symfony controller

How can I choose(filter) on my controller which fields I want (or don't want) to pass to my frontend?
my Controller:
/**
* #Route("/", name="dashboard")
*/
public function index()
{
$aniversariantes = $this->getDoctrine()->getRepository(Usuario::class)->aniversariantes();
return $this->render('dashboard/index.html.twig', [
'controller_name' => 'DashboardController',
'aniversariantes' => $aniversariantes
]);
}
My repository:
/**
* #return []
*/
public function aniversariantes(): array
{
$qb = $this->createQueryBuilder('u')
->andWhere('u.ativo = 1')
->andwhere('extract(month from u.dtNascimento) = :hoje')
->setParameter('hoje', date('m'))
->getQuery();
return $qb->execute();
}
Dump from entity:
What can I do if I don't want to pass the "password" field for example?
If you are just trying to prevent certain fields from being dumped, it is useful to know
Internally, Twig uses the PHP var_dump function.
https://twig.symfony.com/doc/2.x/functions/dump.html
This means you can can define the PHP magic method __debugInfo in your entity
This method is called by var_dump() when dumping an object to get the properties that should be shown. If the method isn't defined on an object, then all public, protected and private properties will be shown.
https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo
So in your entity do something like this:
class Usuario {
...
public function __debugInfo() {
return [
// add index for every field you want to be dumped
// assign/manipulate values the way you want it dumped
'id' => $this->id,
'nome' => $this->nome,
'dtCadastro' => $this->dtCadastro->format('Y-m-d H:i:s'),
];
}
...
}

How to update the mapping information of an entity

I would like to change the default mapping of the Product entity from the Sylius ProductBundle. So I created a listener to the ClassMetadata Event:
<?php
namespace App\Symfony\EventListener\Sylius;
use Sylius\Component\Product\Model\Product;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
/**
* Remove the reference of a Variant from its parent (Product) without delete it
*/
class ProductLoadMetadataSubscriber implements EventSubscriber
{
/**
* #return array
*/
public function getSubscribedEvents()
{
return array(
'loadClassMetadata',
);
}
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
/** #var ClassMetadata $metadata */
$metadata = $eventArgs->getClassMetadata();
if (Product::class !== $metadata->name) {
return;
}
// Property "variants" in "Sylius\Component\Product\Model\Product" was already declared, but it must be declared only once
$metadata->mapOneToMany([
'fieldName' => 'variants',
'targetEntity' => 'Sylius\Component\Product\Model\VariantInterface',
'mappedBy' => 'object',
'orphanRemoval' => false
]);
//$variantsMapping = $metadata->getAssociationMapping('variants');
//$variantsMapping['orphanRemoval'] = false;
}
}
My goal is to set the orphanRemoval option value to false for the variants field.
But I didn't find a method or something else to update the ClassMetadataInfo instance.
At the moment I have this error:
Property "variants" in "Sylius\Component\Product\Model\Product" was
already declared, but it must be declared only once
It's logical because the variants field already exists.
EDIT:
I tried another thing but without success:
unset($metadata->associationMappings['variants']['orphanRemoval']);
// setAssociationOverride doesn't handle 'orphanRemoval' but it calls internally _validateAndCompleteOneToManyMapping
// which set orphanRemoval to false by default
$metadata->setAssociationOverride('variants', []);
To set orphanRemoval, you can try the following snippet:
if (SomeClass::class === $metadata->getName()) {
if (isset($metadata->associationMappings['itemShippingTrackings'])) {
$metadata->associationMappings['itemShippingTrackings']['orphanRemoval'] = false;
}
}

How to display a Many to One Relation in a form dropdown in Symfony2?

I have a a form when I need to display the OneToMany relation between let's say a car and its brand.
When I click on the dropdown for now, I can only see the car name. since it is automatically created from the car entity.
What I'd like to see in my dropdown is NameOfTheCar - NameOfTheBrand. For every choice
I have CarType with a fild like :
$builder->add('cars', 'choice', array(
'choices' => array(
'Test' => 'Example',
),
'required' => true,
));
how can I do that ?
EDIT: I have followed Hugo advices.
Now I got
$builder->add('lake', 'entity', array(
'class' => 'Pondip\KeepnetBundle\Entity\CatchReport',
'required' => true,
));
And my __toString of CatchReport is
public function __toString()
{
return $this->car .' - '. $this->car->getBrand();
}
When lake is
/**
* #var integer
*
* #ORM\ManyToOne(targetEntity="Pondip\CarBundle\Entity\Car")
* #ORM\JoinColumn(name="car_id", referencedColumnName="id")
* #Assert\NotBlank()
*/
private $car;
and in my brand entity I have:
/**
* #var integer
* #Assert\Type(type="Pondip\CarBundle\Entity\Brand")
*
* #ORM\ManyToOne(targetEntity="Pondip\CarBundle\Entity\Brand")
* #ORM\JoinColumn(name="brand_id", referencedColumnName="id")
*/
private $brand;
And now I got an error from my toString() Function. What did I do wrong ?
Thanks
The good way to do it is to use the 'entity' type, and to resolve the whole thing there are 2 main steps.
First step
$builder->add('cars', 'entity', array(
'class' => 'MyBundle:Car',
// 'property' => 'myCustom', -- The default is the __toString() (Enity file)
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('c')
; // You can add anything else here or
// use an existing function of the repository file
),
'required' => true,
));
Normally you can set the key property to the attribute of the object you want to be used, but in your case since it's a bit more complicated you will have to override the __toString() method or make a custom attribute on the object that returns the value and set property => 'myCustom'.
Second Step:
In your entity file, which is not the repository file, but is the Cars.php file (which has the getter setters). This is for the mapping with property.
First: overriding the toString
function __toString(){
return $this->name . " - ". $this->brand;
}
Second: making a custom method
function getMyCustom(){
return $this->name . " - ". $this->brand;
}
Optional
For the query_builder field.
If your query to retrieve the cars is complicated, you might want to make a custom method in your car repository. In order to have the most simple as possible query in the builder. This is to retrieve the list of cars that will populate the dropdown. If you just want to retrieve all the cars, you can just specify ->findAll().
Update
The problem with your __toString seems to be that it can not call functions (I don't know why). Try something along this:
function __toString(){ // CatchReport
return $this->car
}
function __toString(){ // Car
return $this->name . " - " . $this->brand
}
function __toString(){ // Brand
return $this->name
}
This way it never uses the object, and thus can not return exception on null pointer

Sonata Admin Bundle: possible to add a child admin object that can have different parents?

I'm using doctrine inheritance mapping to enable various objects to be linked to a comment entity. This is achieved through various concrete "Threads", which have a one-to-many relationship with comments. So taking a 'Story' element as an example, there would be a related 'StoryThread' entity, which can have many comments.
That is all working fine, but I'm having troubles trying to define a CommentAdmin class for the SonataAdminBundle that can be used as a child of the parent entities. For example, I'd want to be able to use routes such as:
/admin/bundle/story/story/1/comment/list
/admin/bundle/media/gallery/1/comment/list
Does anyone have any pointers about how I can go about achieving this? I'd love to post some code extracts but I haven't managed to find any related documentation so don't really know the best place to start.
I've been trying to use the SonataNewsBundle as a reference because they've implemented a similar parent/child admin relationship between posts and comments, but it appears as though this relies on the 'comment' (child) admin class to be hardcoded to know that it belongs to posts, and it also seems as though it needs to have a direct many-to-one relationship with the parent object, whereas mine is through a separate "Thread" entity.
I hope this makes sense! Thanks for any help.
Ok I managed to get this working eventually. I wasn't able to benefit from using the $parentAssociationMapping property of the CommentAdmin class, as the parent entity of a comment is a concrete instance of the Thread entity whereas the parent 'admin' class in this case is a Story (which is linked via the StoryThread). Plus this will need to remain dynamic for when I implement comments on other types of entity.
First of all, I had to configure my StoryAdmin (and any other admin classes that will have CommentAdmin as a child) to call the addChild method:
acme_story.admin.story:
class: Acme\Bundle\StoryBundle\Admin\StoryAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: content, label: Stories }
arguments: [null, Acme\Bundle\StoryBundle\Entity\Story, AcmeStoryBundle:StoryAdmin]
calls:
- [ addChild, [ #acme_comment.admin.comment ] ]
- [ setSecurityContext, [ #security.context ] ]
This allowed me to link to the child admin section from the story admin, in my case from a side menu, like so:
protected function configureSideMenu(MenuItemInterface $menu, $action, Admin $childAdmin = null)
{
// ...other side menu stuff
$menu->addChild(
'comments',
array('uri' => $admin->generateUrl('acme_comment.admin.comment.list', array('id' => $id)))
);
}
Then, in my CommentAdmin class, I had to access the relevant Thread entity based on the parent object (e.g a StoryThread in this case) and set this as a filter parameter. This is essentially what is done automatically using the $parentAssociationMapping property if the parent entity is the same as the parent admin, which it most likely will be if you aren't using inheritance mapping. Here is the required code from CommentAdmin:
/**
* #param \Sonata\AdminBundle\Datagrid\DatagridMapper $filter
*/
protected function configureDatagridFilters(DatagridMapper $filter)
{
$filter->add('thread');
}
/**
* #return array
*/
public function getFilterParameters()
{
$parameters = parent::getFilterParameters();
return array_merge($parameters, array(
'thread' => array('value' => $this->getThread()->getId())
));
}
public function getNewInstance()
{
$comment = parent::getNewInstance();
$comment->setThread($this->getThread());
$comment->setAuthor($this->securityContext->getToken()->getUser());
return $comment;
}
/**
* #return CommentableInterface
*/
protected function getParentObject()
{
return $this->getParent()->getObject($this->getParent()->getRequest()->get('id'));
}
/**
* #return object Thread
*/
protected function getThread()
{
/** #var $threadRepository ThreadRepository */
$threadRepository = $this->em->getRepository($this->getParentObject()->getThreadEntityName());
return $threadRepository->findOneBy(array(
$threadRepository->getObjectColumn() => $this->getParentObject()->getId()
));
}
/**
* #param \Doctrine\ORM\EntityManager $em
*/
public function setEntityManager($em)
{
$this->em = $em;
}
/**
* #param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
*/
public function setSecurityContext(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
An alternative to your code for direct related entities :
public function getParentAssociationMapping()
{
// we grab our entity manager
$em = $this->modelManager->getEntityManager('acme\Bundle\Entity\acme');
// we get our parent object table name
$className = $em->getClassMetadata(get_class($this->getParent()->getObject($this->getParent()->getRequest()->get('id'))))->getTableName();
// we return our class name ( i lower it because my tables first characted uppercased )
return strtolower( $className );
}
be sure to have your inversedBy variable matching the $className in order to properly work

Categories