Loading dozens of data fixtures in a neat manner - php

I have a Media entity in an app I'm working on that has associations with several other entities: Speaker, Tag, Category, etc.
In the code below I've shown a fixture I've written to create some test-data. It's obviously very long in order to setup and assign the numerous relations between the data.
public function load(ObjectManager $manager)
{
$videoType = new Mediatype();
$videoType->setName('video');
$videoType->setType('video');
$manager->persist($videoType);
$speaker1 = new Speaker();
$speaker1->setName('Joe Bloggs');
$speaker1->setBiography('Joe Bloggs bio.');
$manager->persist($speaker1);
$category1 = new Category();
$category1->setName('PHP');
$category1->setSlug('php');
$manager->persist($category1);
$tag1 = new Tag();
$tag1->setName('PHPNW');
$tag1->setSlug('phpnw');
$manager->persist($tag1);
$video1 = new Media();
$video1->setMediatype($videoType);
$video1->setSpeakers(
new ArrayCollection(
array(
$speaker1
)
)
);
$video1->setCategories(
new ArrayCollection(
array(
$category1
)
)
);
$video1->setTags(
new ArrayCollection(
array(
$tag1
)
)
);
$video1->setDate(new \Datetime());
$video1->setCreationDate(new \DateTime());
$video1->setTitle('My video about PHP');
$video1->setDescription('A video about PHP!');
$video1->setContent('http://some.video-url.com');
$video1->setLength('20:00:00');
$video1->setRating(2.5);
$video1->setVisits(100);
$video1->setLanguage('EN');
$video1->setHostName('PHP');
$video1->setHostUrl('php');
$video1->setStatus('pub');
$manager->persist($video1);
$manager->flush();
}
Now I want to replace this fixture with real data and load a dozen or so Media entities in one fixture. I could copy and paste it a dozen times and make change the data but that's messy and harder to maintain. Is there an nice way to load numerous entities of the same type like this?

I realised that the doctrine/data-fixtures bundle already does exactly what I wanted.
To do this I load each entity in their own fixture and do $this->addReference('admin-user', $user); to access it from another fixture using $this->getReference('admin-user');
Loading fixtures that are dependencies is easy too:
public function getDependencies()
{
// fixture classes that this fixture is dependent on
return array('MyDataFixtures\MyOtherFixture');
}
So now my fixture looks like this:
public function load(ObjectManager $manager)
{
$video1 = new Media();
$video1->setMediatype($this->getReference('video'));
$video1->setSpeakers(
new ArrayCollection(
array(
$this->getReference('joe-bloggs')
)
)
);
$video1->setCategories(
new ArrayCollection(
array(
$this->getReference('php')
)
)
);
$video1->setTags(
new ArrayCollection(
array(
$this->getReference('phpnw')
)
)
);
$video1->setDate(new \Datetime());
$video1->setCreationDate(new \DateTime());
$video1->setTitle('My video about PHP');
$video1->setDescription('A video about PHP!');
$video1->setContent('http://some.video-url.com');
$video1->setLength('20:00:00');
$video1->setRating(2.5);
$video1->setVisits(100);
$video1->setLanguage('EN');
$video1->setHostName('PHP');
$video1->setHostUrl('php');
$video1->setStatus('pub');
$manager->persist($video1);
$manager->flush();
}
/**
* Load this fixtures dependencies
* #see https://github.com/doctrine/data-fixtures
*
* #return array
*/
public function getDependencies()
{
return array(
'...\LoadMediatypeData',
'...\LoadSpeakerData',
'...\LoadCategoryData',
'...\LoadTagData'
);
}

Related

Entity field nullable=true but can not be null. (Symfony)

I have some entities and when I find an interaction (CRUD) for each of them I stock the action in a Logs entity.
My Logs entity has FK to some other entities, and these FK fields can be null.
When I create my new Logs it says:
Expected type 'App\Entity\Client'. Found 'null'.
For the precision, everything works perfectly without the Logs part so I need to fix it to have the result I want.
My Logs entity on where I get the error:
#[ORM\ManyToOne(inversedBy: 'logs', cascade: ['persist'])]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?Client $client = null;
My Client entity:
#[ORM\OneToMany(mappedBy: 'client', targetEntity: Logs::class, orphanRemoval:false, cascade: ['persist'])]
private Collection $logs;
PS: I want the logs to not be deleted in cascade while deleting the stuff it refers to.
EDIT:
In my Group entity:
public function getClient(): ?Client
{
return $this->client;
}
In my Client entity:
/**
* #return Collection<int, Group>
*/
public function getGroups(): Collection
{
return $this->groups;
}
Here is where I get the error:
#[Route('/new', name: 'app_user_new', methods: ['GET', 'POST'])]
public function new(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserRepository $userRepository, LogsRepository $logsRepository, GroupRepository $groupRepository, SiteRepository $siteRepository, ClientRepository $clientRepository): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$userPasswordHasher->hashPassword(
$user,
$form->get('password')->getData()
)
);
$userRepository->save($user, true);
// take all needed information
$now = new DateTime('now');
$action = 'Create "'.$user->getMail().'" user';
$result = 'SUCCESS';
$gw_interaction = 'none';
$client = $clientRepository->clientToCreateAndDelete($clientRepository);
$group = $groupRepository->groupToCreateAndDelete($client, $groupRepository);
$site = $siteRepository->siteToCreateAndDelete($group, $siteRepository);
// write this action on the logs
$logsRepository->createANewLog($now, $action, $result, $gw_interaction, $user, $client, $site);
// delete the factice client
// $groupRepository->remove($group, true);
$clientRepository->remove($client, true);
return $this->redirectToRoute('app_user_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('user/new.html.twig', [
'user' => $user,
'form' => $form,
]);
}
I can't put null as a value so I tried to create the needed entities, add the log and then remove the entities but it also doesn't work. It says I can't delete a parent row before children's (I have a group a child of a client) But if I delete the group and then delete the client, I still have this error. This is why I just want to be able to give a null at the beginning so I do not need to create and delete useless entities.
My function looks like this:
public function createANewLog(DateTime $now, String $action, String $result, String $gw_interaction, User $user)
{
$logs = new Logs();
$logs->setDatetime($now);
$logs->setAction($action);
$logs->setResultat($result);
$logs->setGwInteraction($gw_interaction);
$logs->setHisUser($user);
$this->save($logs, true);
}
One possible solution:
Because I cannot solve this I found another way to save my values. Now, I deleted client and site columns in Logs:
-> when I need them to be null I just do nothing.
-> when I need them to have a value, I wrote the values in the action:
-> before action had for example: "Create site siteName"
-> now action is for example: "Create site siteName from group
GroupName"

Symfony3 form checkbox save error

I tried to look up on Google but didn't find anyone with such a problem. I think I did everything like the documentation guides but I guess I'm missing something
So I have a form with checkbox like this:
$builder->add(
'productTypes',
EntityType::class,
array(
'label' => 'Available for products',
'class' => 'ShopBundle:ProductType',
'choice_label' => 'name',
'multiple' => true,
'expanded' => true,
'by_reference' => false,
)
);
When I'm editing everything goes smooth, I can edit existing entry and check or uncheck this checkbox, it saves properly, but when I try to add new Object I get error:
PHP Fatal error: Call to a member function add() on null in
C:\xampp\htdocs\uniacar-sf\src\ShopBundle\Entity\ProductAttribute.php
on line 188
This is my controller action:
public function editAction(Request $request, $id = null)
{
$this->setMenuTab('cars', 'admin');
$productTypes = new ArrayCollection();
if (!empty($id)) {
$attribute = $this->getRepo(ProductAttribute::class)->find($id);
$this->setTitle('admin.cars.attributes.edit');
foreach ($attribute->getProductTypes() as $value) {
$productTypes->add($value);
}
} else {
$attribute = new ProductAttribute();
$this->setTitle('admin.cars.attributes.new');
}
$form = $this->createForm(ProductAttributeForm::class, $attribute);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$attribute = $form->getData();
foreach ($productTypes as $productType) {
if (false === $attribute->getProductTypes()->contains($productType)) {
$productType->getAttributes()->removeElement($attribute);
$this->db()->persist($productType);
}
}
$this->db()->persist($attribute);
$this->db()->flush();
return $this->redirectToRoute('carAdmin', array('tab' => 'attributes'));
}
$this->setVariables(
array(
'form' => $form->createView(),
'attribute' => $attribute,
)
);
return $this->response();
}
$this->db() is my shortcut for $this->getDoctrine()->getManager()
And this is definition part of ProductAttribute that relates to ProductType:
/**
* Constructor
*/
public function __construct() {
$this->productTypes = new ArrayCollection();
}
/**
* Many Attributes have Many ProductTypes
* #ORM\ManyToMany(targetEntity="ProductType", mappedBy="attributes", cascade={"persist"})
*/
private $productTypes;
/**
* #param ProductType $productType
*/
public function addProductType(ProductType $productType)
{
$this->productTypes->add($productType);
$productType->addProductAttribute($this);
}
/**
* #param ProductType $productType
*/
public function removeProductType(ProductType $productType)
{
$this->productTypes->removeElement($productType);
}
Also there is part of ProductType Entity that relates to ProductAttribute:
/**
* Constructor
*/
public function __construct() {
$this->attributes = new ArrayCollection();
}
/**
* Many ProductTypes have Many Attributes
* #ORM\ManyToMany(targetEntity="ProductAttribute", inversedBy="productTypes")
* #ORM\JoinTable(name="product_type_to_attribute")
*/
private $attributes;
/**
* #param ProductAttribute $attribute
*/
public function addProductAttribute(ProductAttribute $attribute)
{
if (!$this->attributes->contains($attribute)) {
$this->attributes->add($attribute);
}
}
public function removeProductAttribute(ProductAttribute $attribute)
{
$this->attributes->removeElement($attribute);
}
I tried to follow Symfony Embed Form Tutorial (How to Embed a Collection of Forms)
I know that in this case there is no embeded collection (I have another field in this Entity, that is embeded collection of forms and it works just fine) but from what I understand relations are the same in this case, it's many to many so I have to tell the Symfony how to treat relations, add and remove objects.
I dumped data that comes in POST but it's the same as for edition - productType is there. Any ideas why do I get this error?
It fires in ProductAttribute Entity in the line $this->productTypes->add($productType);
EDIT:
I updated the controller code, I messed up the logic about unlinking ProductType from ProductAttribute. But it doesn't have any impact on the problem, still the same 500 error when I try to save new object.
EDIT2:
I can't get stack trace from Symfony because I get ordinary browser 500 error (probably because it's Fatal Error, I found it in apache logs). The line in controller which creates error is $form->handleRequest($request);.
This is not a Collection of Forms, but you are using collection specific method, this is not a good practice, however, you don't need this below code when you create a new object.
foreach ($productTypes as $value) {
if (false === $attribute->getProductTypes()->contains($value)) {
$attribute->getProductTypes()->removeElement($value);
}
}
So, I haven't found solution to the problem but I solved it somehow by fixing file structure of my project (moved bundle's Resources from general Resources folder to Bundle's Resources folder). I have no idea why this fixed the issue and what is even the connection between working but not proper folder structure and submitting forms but now it works, so I will mark the question as answered.

Symfony form not saving entity with ManyToMany relation

I have problem saving entity trough form with ManyToMany relations.
I can not save fields that are on "mappedBy" side of relation.
Code below is not saving anything to database and not trowing any errors:
// Entity/Pet
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Customer", mappedBy="pet", cascade={"persist"})
*/
private $customer;
/**
* Set customer
*
* #param \AppBundle\Entity\Customer $customer
* #return Pet
*/
public function setCustomer($customer)
{
$this->customer = $customer;
return $this;
}
// Entity/Customer
/**
* #var Pet
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Pet", inversedBy="customer", cascade={"persist"})
* #ORM\JoinTable(name="customer_pet",
* joinColumns={
* #ORM\JoinColumn(name="customer_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="pet_id", referencedColumnName="id")
* }
* )
*/
private $pet;
// PetType.php
$builder->add('customer', 'entity',
array(
'class' => 'AppBundle:Customer',
'property' => 'firstname',
'empty_value' => 'Choose owner',
'multiple' => true
));
It is working the other way around. So if I am saving something from CustomerType everything works.
EDIT:
Solution below worked for me but after couple days I found a problem with that solution. If form will be submitted with value that has been already saved in the database then Symfony will trow an error. To prevent that I had to check if given customer has been already assigned to the pet.
Checking of currently assigned customers had to be done on the beginning of function and not after form submission because for some reason after submission Pet() object contains submitted values not only those present in the db.
So on the beginning I've putted all already assigned customers in to the array
$em = $this->getDoctrine()->getManager();
$pet = $em->getRepository('AppBundle:Pet')->find($id);
$petOriginalOwners = array();
foreach ($pet->getCustomer() as $petCustomer)
{
$petOriginalOwners[] = $petCustomer->getId();
}
And after form submission I've checked if submitted ID's are in the array
if ($form->isValid())
{
foreach ($form['customer']->getData()->getValues() as $v)
{
$customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
if ($customer && !in_array($v->getId(), $petOriginalOwners) )
{
$customer->addPet($pet);
}
}
$em->persist($pet);
$em->flush();
return $this->redirect($this->generateUrl('path'));
}
In Symfony2 the entity with the property with the inversedBy doctrine comment is the one that is supposed to EDIT THE EXTRA TABLE CREATED BY THE MANYTOMANY RELATION. That is why when you create a customer it inserts the corresponding rows in that extra table, saving the corresponding pets.
If you want the same behavior to happen the other way around, I recommend:
//PetController.php
public function createAction(Request $request) {
$entity = new Pet();
$form = $this->createCreateForm($entity);
$form->submit($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach ($form['customer']->getData()->getValues() as $v) {
$customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
if ($customer) {
$customer->addPet($entity);
}
}
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('pet_show', array('id' => $entity->getId())));
}
return $this->render('AppBundle:pet:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
private function createCreateForm(Pet $entity) {
$form = $this->createForm(new PetType(), $entity, array(
'action' => $this->generateUrl('pet_create'),
'method' => 'POST',
));
return $form;
}
These two are but standard Symfony2 CRUD-generated actions in the controller corresponding to Pet entity.
The only tweak is the foreach structure inserted in the first action, that way you forcibly add the same pet to each customer you select in the form, thus getting the desired behavior.
Look, it is highly probable THIS is not the RIGHT WAY, or the PROPER WAY, but is A WAY and it works. Hope it helps.
In my case with a services <-> projects scenario, where services has "inversedBy" and projects has "mappedBy" I had to do this in my project controller's edit action so that when editing a project the services you checked would be persisted.
public function editAction(Request $request, Project $project = null)
{
// Check entity exists blurb, and get it from the repository, if you're inputting an entity ID instead of object ...
// << Many-to-many mappedBy hack
$servicesOriginal = new ArrayCollection();
foreach ($project->getServices() as $service) {
$servicesOriginal->add($service);
}
// >> Many-to-many mappedBy hack
$form = $this->createForm(ProjectType::class, $project);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
// << Many-to-many mappedBy hack
foreach ($servicesOriginal as $service) {
if (!$project->getServices()->contains($service)) {
$service->removeProject($project);
$em->persist($service);
}
}
foreach ($project->getServices() as $service) {
$service->addProject($project);
$em->persist($service);
}
// >> Many-to-many mappedBy hack
$em->persist($project);
$em->flush();
return; // I have a custom `redirectWithMessage()` here, use what you like ...
}
return $this->render("Your-template", [
$form => $form->createView(),
$project => $project,
]);
}
This works for both adding and removing entities in the many-to-many from the "mappedBy" side, so EntityType inputs should work as intended.
What's going on here is we're first building an "original" collection containing all of the service entities already linked to for this project. Then when the form is saving we're ensuring:
First that any unchecked services (those in the original collection but not the project object) have the project removed from their internal collection, then persisted.
Second that any newly checked services each add the project to their internal collection, then persisted.
Important: This depends on your entity's addService() and addProject() methods respectively check that each others' collections do not contain duplications. If you don't do this you'll end up with an SQL level error about a duplicate record insertion.
In the service entity I have:
/**
* Add project
*
* #param Project $project
*
* #return Service
*/
public function addProject(Project $project)
{
if (!$this->projects->contains($project)) {
$this->projects->add($project);
}
if (!$project->getServices()->contains($this)) {
$project->getServices()->add($this);
}
return $this;
}
In the project entity I have:
/**
* Add service
*
* #param Service $service
*
* #return Project
*/
public function addService(Service $service)
{
if (!$this->services->contains($service)) {
$this->services->add($service);
}
if (!$service->getProjects()->contains($this)) {
$service->getProjects()->add($this);
}
return $this;
}
You could alternatively check this in your controller instead, but makes sense if the model validates this itself when possible, as the model would break anyway if there were duplicates from any source.
Finally in your controller's create action you'll likely need this bit too just before $em->persist($project). (You won't need to work with an "original" collection as none will exist yet.)
// << Many-to-many mappedBy hack
foreach ($project->getServices() as $service) {
$service->addProject($project);
$em->persist($service);
}
// >> Many-to-many mappedBy hack
I just had the same problem and I solved it differently.
Changing the code in the controller is not the better way to do it.
In my case I have a GenericController that handle all my CRUDs so I can't put in it specific code.
The best way to do it is by adding in your PetType a listener like this :
// PetType.php
$builder->add('customer', 'entity',
array(
'class' => 'AppBundle:Customer',
'property' => 'firstname',
'empty_value' => 'Choose owner',
'multiple' => true
))
->addEventListener( FormEvents::SUBMIT, function( FormEvent $event ) {
/** #var Pet $pet */
$pet = $event->getData();
foreach ( $pet->getCustomers() as $customer ) {
$customer->addPet( $pet );
}
} );
That way you'll keep the mapping logic in the same place.

How to add many files with silverstripe uploadfield

Hello everyone,
I'm trying to add more than 1 file to my uploadfield with this code ->
class FileDo extends File {
static $has_one = array(
'DocumentsFile' => 'DocumentsFile',
);
}
class DocumentsFile extends DataObject {
static $has_one = array(
'DocumentPageAcces1' => 'DocumentPageAcces1'
);
static $has_many = array(
'Files' => 'FileDo'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeByName('DocumentPageAcces1ID');
return $fields;
}
public function onBeforeWrite() {
parent::onBeforeWrite();
$page = DataObject::get_one('DocumentPageAcces1');
if($page) {
$this->DocumentPageAcces1ID = $page->ID;
}
}
}
class DocumentPageAcces1 extends Page {
static $has_many = array(
'DocumentsFiles' => 'DocumentsFile',
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Main', new TextareaField('DocumentsIntro_en', "Document Introduction"));
$fields->addFieldToTab('Root.Main', new TextareaField('PublicationsIntro_en', "Publication Introduction"));
$fields->addFieldToTab('Root.FR', new TextareaField('DocumentsIntro_fr', "Document Introduction"));
$fields->addFieldToTab('Root.FR', new TextareaField('PublicationsIntro_fr', "Publication Introduction"));
$fields->addFieldToTab('Root.NL', new TextareaField('DocumentsIntro_nl', "Document Introduction"));
$fields->addFieldToTab('Root.NL', new TextareaField('PublicationsIntro_nl', "Publication Introduction"));
$upload = new UploadField(
'DocumentsFile',
'Document',
$this->DocumentsFiles()
);
$fields->addFieldToTab('Root.DocumentsFile', $upload);
$fields->removeByName('Content');
$fields->removeByName('Metadata');
return $fields;
}
}
class DocumentPageAcces1_Controller extends Page_Controller {
}
So to make it clear: i'm trying to add some DocumentFile in my DocumentPageAcces1. When i execute this code, i have in my DocumentPageAcces1 the tab DocumentsFiles and in this tab i have the uploadfield.
THE PROBLEM is that the uploadfield doesn't want to keep my file so when i chose some file, i click OK in my finder and nothing happens......Could anyone help me?
Thanks Thomas.
UploadField isn't made to work with DataObjects but like you found with File or it's subclasses.
Although in your case, I don't think you need a subclass, so you could just remove your DocumentFileDo class that extend File and use this in your DocumentPageAcces1.php
static $has_many = array(
'DocumentFiles' => 'File',
);
In the case you want to have more control/details on the files you upload (i.e. add descriptions, titles etc...), in that case you can create a DataObject that has a $has_one relation to a File and then use that DataObject in the relation on your page with a GridField:
DocumentFile.php
class DocumentFile extend DataObject
{
static $db = array(
'Description' => 'HTMLText'
);
static $has_one = array(
'File' => 'File'
);
}
DocumentPageAcces1.php
static $has_many = array(
'DocumentFiles' => 'DocumentFile'
);
function getCMSFields()
{
$fields = parent::getCMSFields();
$c = new GridFieldConfig_RelationEditor();
$f = new GridField('Documents', 'DocumentFiles', $this->DocumentFiles(), $c);
$fields->addFieldToTab('Root.Documents', $f);
return $fields;
}
After some hours I found a solution, maybe it's not the best one but it works for me ->
I created a class that extends the File class for each document type i need so I have for example
class DocumentFileDo extends File {
static $has_one = array(
'DocumentPageAcces1' => 'DocumentPageAcces1'
);
}
In my DocumentPageAcces1.php I have a has_many relationship like this :
static $has_many = array(
'DocumentFileDos' => 'DocumentFileDo',
);
And I the getCMSFields function I have :
$fields->addFieldToTab('Root.Document', new UploadField('DocumentFileDos'));
It works well, you can add many file with just a has_many relationship and an UploadField. But it's not the best i think if you have a hundred different documents type..
PS: With UploadField you can drag and drop files but this function which works well on Chrome does not on Firefox!
I just struggled with the same thing. SilverStripe even has a (faulty*) developer guide on this matter (though it took me hours to find it) … Anyway, I found a solution. (assuming SilverStripe 4)
mysite/page/MyCustomPage.php
class MyCustomPage extends Page
{
private static $many_many = array(
'DocumentFiles' => File::class
);
public function getCMSFields(){
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Main',
UploadField::create('DocumentFiles', 'My Documents')
);
return $fields;
}
}
mysite/extensions/DocumentFileExtension.php
class DocumentFileExtension extends DataExtension
{
private static $belongs_many_many = array(
'DocumentContainers' => MyCustomPage::class
);
// I actually don't know the purpose of the name "DocumentContainers"
// ("Galleries" in the guide); it is never referenced again.
}
mysite/_config/app.yml
SilverStripe\Assets\File:
extensions:
- DocumentFileExtension
themes/mytheme/templates/Layout/MyCustomPage.ss
<% loop $DocumentFiles %>
$Link
<% end_loop %>
You basically extend the native File class into a custom version of it, and "suddenly" the unchanged UploadField (that also does the same for single files) now allows multiple files; complete with a GUI and everything.
Hope this works for you.
* They keep using 'Foo' => 'Bar' all over their official documentation pages instead of 'Foo' => Bar::class, which you have to use in ss4 in order to avoid an Exception:
Uncaught InvalidArgumentException: many_many relation SilverStripe\Blog\Model\BlogPost.DownloadFiles references class File which doesn't exist

Zend Framework 2 - Hydrator strategy for Doctrine relationship not working

As mentioned here I'm building a custom hydration strategy to handle my related objects in a select box in a form.
My form looks like this:
$builder = new AnnotationBuilder($entityManager);
$form = $builder->createForm(new MyEntity());
$form->add(new MyFieldSet());
$hydrator = new ClassMethodsHydrator();
$hydrator->addStrategy('my_attribute', new MyHydrationStrategy());
$form->setHydrator($hydrator);
$form->get('my_attribute')->setValueOptions(
$entityManager->getRepository('SecEntity\Entity\SecEntity')->fetchAllAsArray()
);
When I add a new MyEntity via the addAction everything works great.
I wrote fetchAllAsArray() to populate my selectbox. It lives within my SecEntityRepository:
public function fetchAllAsArray() {
$objects = $this->createQueryBuilder('s')
->add('select', 's.id, s.name')
->add('orderBy', 's.name ASC')
->getQuery()
->getResult();
$list = array();
foreach($objects as $obj) {
$list[$obj['id']] = $obj['name'];
}
return $list;
}
But in the edit-case the extract() function doesn't work. I'm not at the point where I see something of hydrate() so I'll leave it out for now.
My hydrator strategy looks like this:
class MyHydrationStrategy extends DefaultStrategy
{
public function extract($value) {
print_r($value);
$result = array();
foreach ($value as $instance) {
print_r($instance);
$result[] = $instance->getId();
}
return $result;
}
public function hydrate($value) {
...
}
The problem is as follows:
Fatal error: Call to a member function getId() on a non-object
The print_r($value) returns loads of stuff beginning with
DoctrineORMModule\Proxy__CG__\SecEntity\Entity\SecEntity Object
following with something about BasicEntityPersister and somewhere in the mess are my referenced entities.
The print_r($instance) prints nothing. It's just empty. Therefore I guess is the error message legit... but why can't I iterate over these objects?
Any ideas?
Edit:
Regarding to #Sam:
My attribute in the entity:
/**
* #ORM\ManyToOne(targetEntity="Path/To/Entity", inversedBy="whatever")
* #ORM\JoinColumn(name="attribute_id", referencedColumnName="id")
* #Form\Attributes({"type":"hidden"})
*
*/
protected $attribute;
My new selectbox:
$form->add(array(
'name' => 'attribute',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'MyLabel',
'object_manager' => $entityManager,
'target_class' => 'Path/To/Entity',
'property' => 'name'
)
));
My final hope is that I'm doing something wrong within the controller. Neither my selectbox is preselected nor the value is saved...
...
$obj= $this->getEntityManager()->find('Path/To/Entity', $id);
$builder = new \MyEnity\MyFormBuilder();
$form = $builder->newForm($this->getEntityManager());
$form->setBindOnValidate(false);
$form->bind($obj);
$form->setData($obj->getArrayCopy());
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$form->bindValues();
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('entity');
}
}
I still haven't come around to write the tutorial for that :S
I don't know if this is working with the annotationbuilder though! As the DoctrineModule\Form\Element\ObjectSelect needs the EntityManager to work. The options for the ObjectSelect are as follows:
$this->add(array(
'name' => 'formElementName',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'formElementLabel',
'empty_option' => '--- choose formElementName ---',
'object_manager' => $this->getEntityManager(),
'target_class' => 'Mynamespace\Entity\Entityname',
'property' => 'nameOfEntityPropertyAsSelect'
)
));
In this case i make use of $this->getEntityManager(). I set up this dependency when calling the form from the ServiceManager. Personally i always do this from FactoryClasses. My FormFactory looks like this:
public function createService(ServiceLocatorInterface $serviceLocator)
{
$em = $serviceLocator->get('Doctrine\ORM\EntityManager');
$form = new ErgebnishaushaltProduktForm('ergebnisform', array(
'entity_manager' => $em
));
$classMethodsHydrator = new ClassMethodsHydrator(false);
// Wir fügen zwei Strategien, um benutzerdefinierte Logik während Extrakt auszuführen
$classMethodsHydrator->addStrategy('produktBereich', new Strategy\ProduktbereichStrategy())
->addStrategy('produktGruppe', new Strategy\ProduktgruppeStrategy());
$hydrator = new DoctrineEntity($em, $classMethodsHydrator);
$form->setHydrator($hydrator)
->setObject(new ErgebnishaushaltProdukt())
->setInputFilter(new ErgebnishaushaltProduktFilter())
->setAttribute('method', 'post');
return $form;
}
And this is where all the magic is happening. Magic, that is also relevant to your other Thread here on SO. First, i grab the EntityManager. Then i create my form, and inject the dependency for the EntityManager. I do this using my own Form, you may write and use a Setter-Function to inject the EntityManager.
Next i create a ClassMethodsHydrator and add two HydrationStrategies to it. Personally i need to apply those strategies for each ObjectSelect-Element. You may not have to do this on your side. Try to see if it is working without it first!
After that, i create the DoctrineEntity-Hydrator, inject the EntityManager as well as my custom ClassMethodsHydrator. This way the Strategies will be added easily.
The rest should be quite self-explanatory (despite the german classnames :D)
Why the need for strategies
Imo, this is something missing from the DoctrineEntity currently, but things are still in an early stage. And once DoctrineModule-Issue#106 will be live, things will change again, probably making it more comfortable.
A Strategy looks like this:
<?php
namespace Haushaltportal\Stdlib\Hydrator\Strategy;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class ProduktbereichStrategy implements StrategyInterface
{
public function extract($value)
{
if (is_numeric($value) || $value === null) {
return $value;
}
return $value->getId();
}
public function hydrate($value)
{
return $value;
}
}
So whenever the $value is not numeric or null, meaning: it should be an Object, we will call the getId() function. Personally i think it's a good idea to give each Element it's own strategy, but if you are sure you won't be needing to change the strategy at a later point, you could create a global Strategy for several elements like DefaultGetIdStrategy or something.
All this is basically the good work of Michael Gallego aka Bakura! In case you drop by the IRC, just hug him once ;)
Edit An additional resource with a look into the future - updated hydrator-docs for a very likely, soon to be included, pull request

Categories