I've been tring the last days to make SonataMedia works with Symfony 2.0.16... with no success. Googling around seems like no much people use that bundle or there's a tutorial or an how-to that I'm not aware of, cause I don't get to much info about the error messages I've got so far.
Anyway, my last attempt gave the next error message:
The current field `path` is not linked to an admin. Please create one for the target entity : ``
"path" is the field used to save the file image (relative) path.
AttachmentAdmin.php
<?php
class AttachmentAdmin extends Admin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add(
'path',
'sonata_type_collection',
array(
'required' => true
),
array(
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'targetEntity' => 'Application\Sonata\MediaBundle\Entity\GalleryHasMedia',
'link_parameters' => array(
'context' => 'attachment'
)
)
)
->add('notes', 'textarea', array('required' => false))
;
}
// other methods
}
Attachment.php
<?php
class Attachment
{
// other properties
/**
* #var string $path
*
* #ORM\Column(name="path", type="string", nullable=false)
* #ORM\ManyToOne(targetEntity="Application\Sonata\MediaBundle\Entity\GalleryHasMedia", cascade={"persist"})
*/
protected $path;
// other methods
/**
* Set path
*
* #param string $path
*/
public function setPath($path)
{
$this->path = $path;
foreach ($path as $ent) {
$ent->setAttachment($this);
}
}
/**
*
* #return string
*/
public function getPath()
{
return $this->path;
}
/**
*
* #param \Application\Sonata\MediaBundle\Entity\GalleryHasMedia $path
*/
public function addPath(\Application\Sonata\MediaBundle\Entity\GalleryHasMedia $path)
{
$this->path[] = $path;
}
}
GalleryHasMedia.php
<?php
class GalleryHasMedia extends BaseGalleryHasMedia
{
/**
* #var integer $id
*/
protected $id;
/**
*
* #var File
*/
private $attachment;
/**
* Get id
*
* #return integer $id
*/
public function getId()
{
return $this->id;
}
/**
*
* #param \Mercury\CargoRecognitionBundle\Entity\Attachment $attachment
* #return \Application\Sonata\MediaBundle\Entity\GalleryHasMedia
*/
public function setAttachment(\Mercury\CargoRecognitionBundle\Entity\Attachment $attachment = null)
{
$this->attachment = $attachment;
return $this;
}
/**
*
* #return \Application\Sonata\MediaBundle\Entity\File
*/
public function getAttachment()
{
return $this->attachment;
}
}
services.yml
mercury.cargo_recognition.admin.attachment:
class: Mercury\CargoRecognitionBundle\Admin\AttachmentAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: General, label: Attachments }
arguments: [ null, Mercury\CargoRecognitionBundle\Entity\Attachment, "MercuryCargoRecognitionBundle:AttachmentAdmin" ]
Thanks for any info!
Just a wild guess, create an admin class for GalleryHasMedia entity.
You need to add admin_code like this
$formMapper
->add(
'path',
'sonata_type_collection',
array(
'required' => true
),
array(
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'targetEntity' => 'Application\Sonata\MediaBundle\Entity\GalleryHasMedia',
'link_parameters' => array(
'context' => 'attachment'
),
'admin_code' => 'sonata.media.admin.gallery_has_media' // this will be your admin class service name
)
)
->add('notes', 'textarea', array('required' => false))
;
You are trying to apply 'sonata_type_collection' on field 'path' of same entity class 'attachment' , while 'sonata_type_collection' is for collection of embeded form of different class .
So you will have to one more entity class suppose 'AttachmentCollection' and in this particular AttachmentsCollection's admin class, you should embed the 'Attachment' class admin ..
example:
class AttachmentsCollection extends Admin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('attachments', 'sonata_type_collection', array(
'required' => false,
'type_options' => array('delete' => true)
), array(
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
));
}
}
And also do't forget to do mapping 'one to many' or 'many to many' mapping between 'AttachmentsCollection' and 'Attachments' supposing that one 'AttachmentsCollection' has many 'Attachments' object ..
Looks like what you need to do is pass in an admin_code which is the name of the admin section (mercury.cargo_recognition.admin.attachment) in the $fieldDescriptionOptions parameter to the add() method.
As old as this issue is, I'll still add something on how this can be resolved to help anyone else who might run into it. The root issue is that the $path targetEntity("Application\Sonata\MediaBundle\Entity\GalleryHasMedia") doesn't have an admin class.
To resolve it, add an admin class for your targetEntity:
class GalleryHasMediaAdmin extends Admin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('attachment', 'file')
;
}
// other methods
}
Then add that admin class to services.yml
mercury.cargo_recognition.admin.galleryhadmedia:
class: Mercury\CargoRecognitionBundle\Admin\GalleryHasMediaAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: General, label: 'Gallery Has Media' }
arguments: [ null, Mercury\CargoRecognitionBundle\Entity\GalleryHasMedia, "MercuryCargoRecognitionBundle:GalleryHasMediaAdmin" ]
if your admin class for GalleryHasMedia is not yet created and you have not put the service code in your config.yml file then first do that and try again. I had the same issue and got resolved this way.
Five years later, it's worth noting that the "target entity" itself appears to be blank in the error text. We get a set of backticks, rather than an entity name.
(I am working on an old app and getting a similar error right now. I have generated an admin class for my entity and registered it in the correct place, and it looks like the app is having trouble figuring out what my target entity is -- which by default prevents it from finding the related admin class. I'm sort of tearing my hair out over this one and will probably put up a bounty tomorrow on it.)
Related
I'm really confused about my Form Filter.
My Test-Project contains 2 Models.
class Category extends AbstractEntity
{
use Nameable; // just property name and getter and setter
/**
* #var boolean
* #ORM\Column(name="issue", type="boolean")
*/
private $issue;
/**
* #var Collection|ArrayCollection|Entry[]
*
* #ORM\OneToMany(targetEntity="CashJournal\Model\Entry", mappedBy="category", fetch="EAGER", orphanRemoval=true, cascade={"persist", "remove"})
*/
private $entries;
}
the entry
class Entry extends AbstractEntity
{
use Nameable;
/**
* #var null|float
*
* #ORM\Column(name="amount", type="decimal")
*/
private $amount;
/**
* #var null|Category
*
* #ORM\ManyToOne(targetEntity="CashJournal\Model\Category", inversedBy="entries", fetch="EAGER")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
*/
protected $category;
/**
* #var null|DateTime
*
* #ORM\Column(name="date_of_entry", type="datetime")
*/
private $dateOfEntry;
}
And if someone needed the AbstractEntity
abstract class AbstractEntity implements EntityInterface
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
}
Every Category can have many Entries. I'm using Doctrine for this relation. And this works fine.
I have a Form based on this FieldSet:
$this->add([
'name' => 'id',
'type' => Hidden::class
]);
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name'
]
]);
$this->add([
'name' => 'amount',
'type' => Number::class,
'options' => [
'label' => 'Summe'
]
]);
$this->add([
'name' => 'date_of_entry',
'type' => Date::class,
'options' => [
'label' => 'Datum'
]
]);
$this->add([
'name' => 'category',
'type' => ObjectSelect::class,
'options' => [
'target_class' => Category::class,
]
]);
So my Form displays a dropdown with my categories. Yeah fine.
To load the Category for my Entry Entity i use a filter.
$this->add([
'name' => 'category',
'required' => true,
'filters' => [
[
'name' => Callback::class,
'options' => [
'callback' => [$this, 'loadCategory']
]
]
]
]);
And the callback:
public function loadCategory(string $categoryId)
{
return $this->mapper->find($categoryId);
}
The mapper loads the category fine. great. But the form is invalid because:
Object of class CashJournal\Model\Category could not be converted to int
Ok, so i'm removing the Filter, but now it failed to set the attributes to the Entry Entity, because the setter needs a Category. The Form error says:
The input is not a valid step
In Symfony i can create a ParamConverter, which converts the category_id to an valid Category Entity.
Question
How i can use the filter as my ParamConver?
Update
Also when i cast the category_id to int, i will get the error from the form.
Update 2
I changed my FieldSet to:
class EntryFieldSet extends Fieldset implements ObjectManagerAwareInterface
{
use ObjectManagerTrait;
/**
* {#inheritDoc}
*/
public function init()
{
$this->add([
'name' => 'id',
'type' => Hidden::class
]);
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name'
]
]);
$this->add([
'name' => 'amount',
'type' => Number::class,
'options' => [
'label' => 'Summe'
]
]);
$this->add([
'name' => 'date_of_entry',
'type' => Date::class,
'options' => [
'label' => 'Datum'
]
]);
$this->add([
'name' => 'category',
'required' => false,
'type' => ObjectSelect::class,
'options' => [
'target_class' => Category::class,
'object_manager' => $this->getObjectManager(),
'property' => 'id',
'display_empty_item' => true,
'empty_item_label' => '---',
'label_generator' => function ($targetEntity) {
return $targetEntity->getName();
},
]
]);
parent::init();
}
}
But this will be quit with the error message:
Entry::setDateOfEntry() must be an instance of DateTime, string given
Have you checked the documentation for ObjectSelect? You appear to be missing a few options, namely which hydrator (EntityManager) and identifying property (id) to use. Have a look here.
Example:
$this->add([
'type' => ObjectSelect::class,
'name' => 'category', // Name of property, 'category' in your question
'options' => [
'object_manager' => $this->getObjectManager(), // Make sure you provided the EntityManager to this Fieldset/Form
'target_class' => Category::class, // Entity to target
'property' => 'id', // Identifying property
],
]);
To validate selected Element, add in your InputFilter:
$this->add([
'name' => 'category',
'required' => true,
]);
No more is needed for the InputFilter. A Category already exist and as such has been validated before. So, you should just be able to select it.
You'd only need additional filters/validators if you have special requirements, for example: "A Category may only be used once in Entries", making it so that you need to use a NoObjectExists validator. But that does not seem to be the case here.
UPDATE BASED ON COMMENTS & PAST QUESTIONS
I think you're over complicating a lot of things in what you're trying to do. It seems you want to simply populate a Form before you load it client-side. On receiving a POST (from client) you wish to put the received data in the Form, validate it and store it. Correct?
Based on that, please find a complete controller for User that I have in one of my projects. Hope you find it helpful. Providing it because updates are veering away from your original question and this might help you out.
I've removed some additional checking and error throwing, but otherwise is in complete working fashion.
(Please note that I'm using my own abstract controller, make sure to replace it with your own and/or recreate and match requirements)
I've also placed additional comments throughout this code to help you out
<?php
namespace User\Controller\User;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\ORMException;
use Exception;
use Keet\Mvc\Controller\AbstractDoctrineActionController;
use User\Entity\User;
use User\Form\UserForm;
use Zend\Http\Request;
use Zend\Http\Response;
class EditController extends AbstractDoctrineActionController
{
/**
* #var UserForm
*/
protected $userEditForm; // Provide this
public function __construct(ObjectManager $objectManager, UserForm $userEditForm)
{
parent::__construct($objectManager); // Require this in this class or your own abstract class
$this->setUserEditForm($userEditForm);
}
/**
* #return array|Response
* #throws ORMException|Exception
*/
public function editAction()
{
$id = $this->params()->fromRoute('id', null);
// check if id set -> else error/redirect
/** #var User $entity */
$entity = $this->getObjectManager()->getRepository(User::class)->find($id);
// check if entity -> else error/redirect
/** #var UserForm $form */
$form = $this->getUserEditForm(); // GET THE FORM
$form->bind($entity); // Bind the Entity (object) on the Form
// Only go into the belof if() on POST, else return Form. Above the data is set on the Form, so good to go (pre-filled with existing data)
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost()); // Set received POST data on Form
if ($form->isValid()) { // Validates Form. This also updates the Entity (object) with the received POST data
/** #var User $user */
$user = $form->getObject(); // Gets updated Entity (User object)
$this->getObjectManager()->persist($user); // Persist it
try {
$this->getObjectManager()->flush(); // Store in DB
} catch (Exception $e) {
throw new Exception('Could not save. Error was thrown, details: ', $e->getMessage());
}
return $this->redirectToRoute('users/view', ['id' => $user->getId()]);
}
}
// Returns the Form with bound Entity (object).
// Print magically in view with `<?= $this->form($form) ?>` (prints whole Form!!!)
return [
'form' => $form,
];
}
/**
* #return UserForm
*/
public function getUserEditForm() : UserForm
{
return $this->userEditForm;
}
/**
* #param UserForm $userEditForm
*
* #return EditController
*/
public function setUserEditForm(UserForm $userEditForm) : EditController
{
$this->userEditForm = $userEditForm;
return $this;
}
}
Hope that helps...
I'm using Sonata admin in my Symfony project. I have 2 entities like Parent and Child. Parent entity is connected to child by one-to-many relationship.
I have created 2 admin classes for child entity with different baseRoutName. I need to use Child entity fields in Parent entity sonata form for 2 times.
//ParentAdmin.php
$formMapper
->with('Child 1', ['class' => 'col-md-4'])
->add('child', CollectionType::class, [], [
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'admin_code' => 'admin.child1'
])
->end()
->with('Child 2', ['class' => 'col-md-4'])
->add('child', CollectionType::class, [], [
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'admin_code' => 'admin.child2'
])
->end();
The problem here is that I need to use child field for multiple times. But the child field within Child 2 is overriding the child field in Child 1. As you can see I have used different admin_code for these 2 fields.
My expected output is,
But the actual output I'm getting is,
I know the problem here is duplicate entity fields. Is it possible to display same field for multiple times?
Does anyone have solution/suggestion? Thanks in advance!!
maybe it's late but i had the same problem, found this post with no answer, and finally found a solution, so here it is for future purpose.
i have a Request class with generated document, get an eye on get and add functions:
class Request
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="GeneratedDocument", mappedBy="request", cascade={"all"}, orphanRemoval=true)
*/
protected $generatedDocuments;
/**
* #var ArrayCollection
* each of this attributes will get one type of $generatedDocuments
*/
protected $generatedAttestationDocuments;
protected $generatedCertificationDocuments;
public function __construct()
{
$this->generatedDocuments = new ArrayCollection();
}
/**
* Set the value of id.
*
* #param integer $id
* #return \App\Entity\Request
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get the value of id.
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* #return Collection|GeneratedDocument[]
*/
public function getGeneratedDocuments(): Collection
{
return $this->generatedDocuments;
}
public function addGeneratedDocument(GeneratedDocument $generatedDocument): self
{
if (!$this->generatedDocuments->contains($generatedDocument)) {
$this->generatedDocuments[] = $generatedDocument;
$generatedDocument->setRequest($this);
}
return $this;
}
public function removeGeneratedDocument(GeneratedDocument $generatedDocument): self
{
if ($this->generatedDocuments->contains($generatedDocument)) {
$this->generatedDocuments->removeElement($generatedDocument);
// set the owning side to null (unless already changed)
if ($generatedDocument->getRequest() === $this) {
$generatedDocument->setRequest(null);
}
}
return $this;
}
/**
* #return Collection|GeneratedDocument[]
*
* #param int $type
*/
protected function getTypedGeneratedDocuments(int $type): Collection
{
return $this->getGeneratedDocuments()->filter(function (GeneratedDocument $gd) use ($type) {
if ($gd->getGeneratedDocumentModel()) {
return $type === $gd->getGeneratedDocumentModel()->getType();
}
return false;
});
}
/**
* #return Collection|GeneratedDocument[]
*/
public function getGeneratedAttestationDocuments(): Collection
{
if (empty($this->generatedAttestationDocuments)) {
$this->generatedAttestationDocuments =
$this->getTypedGeneratedDocuments(GeneratedDocumentModel::TYPE_ATTESTATION);
}
return $this->generatedAttestationDocuments;
}
public function addGeneratedAttestationDocument(GeneratedDocument $generatedDocument): self
{
$this->generatedAttestationDocuments[] = $generatedDocument;
return $this->addGeneratedDocument($generatedDocument);
}
public function removeGeneratedAttestationDocument(GeneratedDocument $generatedDocument): self
{
return $this->removeGeneratedDocument($generatedDocument);
}
/**
* #return Collection|GeneratedDocument[]
*/
public function getGeneratedCertificationDocuments(): Collection
{
if (empty($this->generatedCertificationDocuments)) {
$this->generatedCertificationDocuments =
$this->getTypedGeneratedDocuments(GeneratedDocumentModel::TYPE_CERTIFICATE);
}
return $this->generatedCertificationDocuments;
}
public function addGeneratedCertificationDocument(GeneratedDocument $generatedDocument): self
{
$this->generatedCertificationDocuments[] = $generatedDocument;
return $this->addGeneratedDocument($generatedDocument);
}
public function removeGeneratedCertificationDocument(GeneratedDocument $generatedDocument): self
{
return $this->removeGeneratedDocument($generatedDocument);
}
}
Then in admin you have to give a different name in each add, here I use my typed generated documents so there's no duplication problem, but they are not mapped and symfony complain about it.
Trying to use 'mapped' => false resolved nothing, the simplest way I found was a 'virtual mapping', based on the original mapped attribute 'generatedDocuments, just the time to fool symfony when building the form.
class RequestAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper): void
{
/** #var Request $createdRequest */
$createdRequest = $this->getSubject();
$metaData = $this->getModelManager()->getMetadata($this->getClass());
//We need many CollectionType based on 'generatedDocuments', and we need an ArrayCollection for each of them
//so here is a virtual mapping to make symfony accept the persistence of our CollectionType
//then setter should fill 'generatedDocuments'
$mapping = $metaData->getAssociationMappings()['generatedDocuments'];
$mapping['fieldName'] = 'generatedAttestationDocuments';
$metaData->mapOneToMany($mapping);
$mapping['fieldName'] = 'generatedCertificationDocuments';
$metaData->mapOneToMany($mapping);
$formMapper
->with(('Attestation Deposit'))
->add('generatedAttestationDocuments', CollectionType::class, [
'label' => false,
'by_reference' => false,
'btn_add' => 'Add Attestation',
'data' => $createdRequest->getGeneratedAttestationDocuments(),
], [
'edit' => 'inline',
'inline' => 'table',
'admin_code' => 'admin.generated_document_attestation',
])
->end()
->with(('Certificate'))
->add('generatedCertificationDocuments', CollectionType::class, [
'label' => false,
'by_reference' => false,
'btn_add' => 'Add Certification',
'data' => $createdRequest->getGeneratedCertificationDocuments(),
], [
'edit' => 'inline',
'inline' => 'table',
'admin_code' => 'admin.generated_document_certification',
])
->end()
//delete virtual mapping to avoid it to get handle like a real mapping
unset($metaData->associationMappings['generatedAttestationDocuments']);
unset($metaData->associationMappings['generatedCertificationDocuments']);
}
}
I would like to know a simplest way, but It really work like individual CollectionType for me.
Hope it will help!
I try edit record in Symfony .i can edit text record but I cannot edit a new file on record.((file saved in Storage but don't save on database))
How i can do that in Symfony 3.4 with VichUploaderBundle 1.4 ...
for insert, I have not any problem I upload the image to storage and save in database
.....
it is my editAction
/**
* #Route("/Article/editArticle/{id}",name="editArticle")
*/
public function editArticleAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$articleRepo=$em->getRepository('AdminBundle:Article');
$articleata = $articleRepo->find($id);
// $satelliteImage=new satelliteImage;
$article = new Article();
$form = $this->createForm(ArticleType::class,$article,array(
'action' => $this->generateUrl('editArticle',array('id' => $articleata->getId())),
'attr' => array(
'class' => 'dropzone',
'id' => "my-awesome-dropzone"
),
'method' => 'POST',
[
'name' => $articleata->getName(),
'title' => $articleata->getTitle(),
'subject' => $articleata->getSubject(),
'description' => $articleata->getDescription(),
'smallPic' => $articleata->getSmallPic(),
'largPic' => $articleata->getLargPic(),
'displayStatus' => $articleata->getDisplayStatus()]
));
//
if ($request->getMethod() == Request::METHOD_POST){
$form->handleRequest($request);
$article->setName($form->get('name')->getData());
$article->setTitle($form->get('title')->getData());
$article->setSubject($form->get('subject')->getData());
$article->setDescription($form->get('description')->getData());
$article->setDisplayStatus($form->get('displayStatus')->getData());
$article->setSmallPic($form->get('imageFile')->getData());
$article->setLargPic($form->get('imageFile2')->getData());
$em = $this->getDoctrine()->getManager();
$em->flush();
}
return $this->render('AdminBundle:Article:edit_article.html.twig', array(
'form' => $form->createView(),
'Articles' => $articleata
));
}
my Entity
private $smallPic;
/**
* #Vich\UploadableField(mapping="articles_images", fileNameProperty="smallPic")
* #var File
*/
private $imageFile;
/**
* #ORM\Column(type="datetime")
* #var \DateTime
*/
private $updatedAt;
/**
* #var string
*
* #ORM\Column(name="largPic", type="string", length=255)
*/
private $largPic;
/**
* #Vich\UploadableField(mapping="articles_images", fileNameProperty="largPic")
* #var File
*/
private $imageFile2;
public function setImageFile(File $smallPic = null)
{
$this->imageFile = $smallPic;
// VERY IMPORTANT:
// It is required that at least one field changes if you are using Doctrine,
// otherwise the event listeners won't be called and the file is lost
if ($smallPic) {
// if 'updatedAt' is not defined in your entity, use another property
$this->updatedAt = new \DateTime('now');
}
}
public function getImageFile()
{
return $this->imageFile;
}
public function setSmallPic($smallPic)
{
$this->smallPic = $smallPic;
}
public function getSmallPic()
{
return $this->smallPic;
}
public function setImageFile2(File $largPic = null)
{
$this->imageFile2 = $largPic;
// VERY IMPORTANT:
// It is required that at least one field changes if you are using Doctrine,
// otherwise the event listeners won't be called and the file is lost
if ($largPic) {
// if 'updatedAt' is not defined in your entity, use another property
$this->updatedAt = new \DateTime('now');
}
}
public function getImageFile2()
{
return $this->imageFile2;
}
/**
* Set largPic
*
* #param string $largPic
*
* #return Article
*/
public function setLargPic($largPic)
{
$this->largPic = $largPic;
}
/**
* Get largPic
*
* #return string
*/
public function getLargPic()
{
return $this->largPic;
}
.
.
.
FromType
->add('imageFile', VichFileType::class, array(
'label' => "smallpic",
'required' => false,
'allow_delete' => true, // not mandatory, default is true
'download_link' => true, // not mandatory, default is true
))
->add('imageFile2', VichFileType::class, array(
'label' => "largPic",
'required' => false,
'allow_delete' => true, // not mandatory, default is true
'download_link' => true, // not mandatory, default is true
))
config.yml
vich_uploader:
db_driver: orm
mappings:
articles_images:
uri_prefix: '%app.path.articles_images%'
upload_destination: '%kernel.root_dir%/../web/uploads/images/articles'
inject_on_load: true
delete_on_update: true
delete_on_remove: true
Before the flush you need to:
$em->persist($article);
UPDATED:
In general, VichUploader Bundle is not considered best practice to upload images anymore. Storing image data in the DB really bloats up it's size, and it's not recommended. You have another entity called media, with some fields like hash, path, size, etc... and you should use the filesystem, with an abstraction if you want to change to, for example, Amazon S3 or Google Storage in the future.
Take a look at the aswesome flysystem bundle that uses the flysystem library.
Extending on 2 previous questions about form structure and validating collections I've run into the next issue.
My form validates properly. Including included collections by way of Fieldsets. But the innermost Fieldset should not result in an Entity and a FK association to the parent if its values are not set.
An Address may or may not have linked Coordinates. It's possible to create all of these in the same Form.
However, the Coordinates should not be created and should not be linked from the Address if no coordinates have been given in the Form. They're not required in the Form and the Entity Coordinates itself requires both properties of Latitude and Longitude to be set.
Below, first the Entities. Following are the Fieldsets used for the AddressForm. I've removed stuff from both unrelated to the chain of Address -> Coordinates.
Address.php
class Address extends AbstractEntity
{
// Properties
/**
* #var Coordinates
* #ORM\OneToOne(targetEntity="Country\Entity\Coordinates", cascade={"persist"}, fetch="EAGER", orphanRemoval=true)
* #ORM\JoinColumn(name="coordinates_id", referencedColumnName="id", nullable=true)
*/
protected $coordinates;
// Getters/Setters
}
Coordinates.php
class Coordinates extends AbstractEntity
{
/**
* #var string
* #ORM\Column(name="latitude", type="string", nullable=false)
*/
protected $latitude;
/**
* #var string
* #ORM\Column(name="longitude", type="string", nullable=false)
*/
protected $longitude;
// Getters/Setters
}
As is seen in the Entities above. An Address has a OneToOne uni-directional relationship to Coordinates. The Coordinates entity requires both latitude and longitude properties, as seen with the nullable=false.
It's there that it goes wrong. If an Address is created, but no Coordinates's properties are set in the form, it still creates a Coordinates Entity, but leaves the latitude and longitude properties empty, even though they're required.
So, in short:
A Coordinates Entity is created where non should exist
A link to Coordinates is created from Address where non should exist
Below the Fieldsets and InputFilters to clarify things further.
AddressFieldset.php
class AddressFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
// Other properties
$this->add([
'type' => CoordinatesFieldset::class,
'required' => false,
'name' => 'coordinates',
'options' => [
'use_as_base_fieldset' => false,
],
]);
}
}
CoordinatesFieldset.php
class CoordinatesFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
$this->add([
'name' => 'latitude',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Latitude'),
],
]);
$this->add([
'name' => 'longitude',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Longitude'),
],
]);
}
}
AddressFieldsetInputFilter.php
class AddressFieldsetInputFilter extends AbstractFieldsetInputFilter
{
/** #var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
protected $coordinatesFieldsetInputFilter;
public function __construct(
CoordinatesFieldsetInputFilter $filter,
EntityManager $objectManager,
Translator $translator
) {
$this->coordinatesFieldsetInputFilter = $filter;
parent::__construct([
'object_manager' => $objectManager,
'object_repository' => $objectManager->getRepository(Address::class),
'translator' => $translator,
]);
}
/**
* Sets AddressFieldset Element validation
*/
public function init()
{
parent::init();
$this->add($this->coordinatesFieldsetInputFilter, 'coordinates');
// Other filters/validators
}
}
CoordinatesFieldsetInputFilter.php
class CoordinatesFieldsetInputFilter extends AbstractFieldsetInputFilter
{
public function init()
{
parent::init();
$this->add([
'name' => 'latitude',
'required' => true,
'allow_empty' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 2,
'max' => 255,
],
],
[
'name' => Callback::class,
'options' => [
'callback' => function($value, $context) {
//If longitude has a value, mark required
if(empty($context['longitude']) && strlen($value) > 0) {
$validatorChain = $this->getInputs()['longitude']->getValidatorChain();
$validatorChain->attach(new NotEmpty(['type' => NotEmpty::NULL]));
$this->getInputs()['longitude']->setValidatorChain($validatorChain);
return false;
}
return true;
},
'messages' => [
'callbackValue' => _('Longitude is required when setting Latitude. Give both or neither.'),
],
],
],
],
]);
// Another, pretty much identical function for longitude (reverse some params and you're there...)
}
}
EDIT: Adding a DB Dump image. Shows empty latitude, longitude.
EDIT2: When I remove 'allow_empty' => true, from the AddressFieldsetInputFilter inputs and fill a single input (latitude or longitude), then it validates correctly, unless you leave both inputs empty, then it breaks off immediately to return that the input is required. (Value is required and can't be empty).
By chance did I stumple upon this answer, which was for allowing a Fieldset to be empty but validate it if at least a single input was filled in.
By extending my own AbstractFormInputFilter and AbstractFieldsetInputFilter classes from an AbstractInputFilter class, which incorporates the answer, I'm now able to supply FielsetInputFilters, such as the AddressFieldsetInputFilter, with an additional ->setRequired(false). Which is then validated in the AbstractInputFilter, if it actually is empty.
The linked answer gives this code:
<?php
namespace Application\InputFilter;
use Zend\InputFilter as ZFI;
class InputFilter extends ZFI\InputFilter
{
private $required = true;
/**
* #return boolean
*/
public function isRequired()
{
return $this->required;
}
/**
* #param boolean $required
*
* #return $this
*/
public function setRequired($required)
{
$this->required = (bool) $required;
return $this;
}
/**
* #return bool
*/
public function isValid()
{
if (!$this->isRequired() && empty(array_filter($this->getRawValues()))) {
return true;
}
return parent::isValid();
}
}
As I mentioned I used this code to extend my own AbstractInputFilter, allowing small changes in *FieldsetInputFilterFactory classes.
AddressFieldsetInputFilterFactory.php
class AddressFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
/**
* #param ServiceLocatorInterface|ControllerManager $serviceLocator
* #return InputFilter
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
parent::setupRequirements($serviceLocator, Address::class);
/** #var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
$coordinatesFieldsetInputFilter = $this->getServiceManager()->get('InputFilterManager')
->get(CoordinatesFieldsetInputFilter::class);
$coordinatesFieldsetInputFilter->setRequired(false); // <-- Added option
return new AddressFieldsetInputFilter(
$coordinatesFieldsetInputFilter,
$this->getEntityManager(),
$this->getTranslator()
);
}
}
Might not be a good idea for everybody's projects, but it solves my problem of not always wanting to validate a Fieldset and it definitely solves the original issue of not creating an Entity with just an ID, as shown in the screenshot in the question.
I have a zf2 application that works with doctrine.
I have the following entity:
class Role
{
/**
* #var int
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=255, unique=true, nullable=true)
*/
protected $name;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="YrmUser\Entity\Role", mappedBy="parent")
*/
protected $children;
/**
* #var Role
* #ORM\ManyToOne(targetEntity="YrmUser\Entity\Role", inversedBy="children", cascade={"persist"})
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
protected $parent;
}
for this entity i have a form:
class RoleForm extends Form
{
/**
* [init description]
*
* #return void
*/
public function init()
{
$this->setHydrator(
new DoctrineHydrator($this->objectManager, 'YrmUser\Entity\Role')
)->setObject(new Role());
$this->setAttribute('method', 'post');
$this->add(
array(
'name' => 'name',
'attributes' => array(
'type' => 'text',
'placeholder' =>'Name',
),
'options' => array(
'label' => 'Name',
),
)
);
$this->add(
array(
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'name' => 'parent',
'attributes' => array(
'id' => 'parent_id',
),
'options' => array(
'label' => 'Parent',
'object_manager' => $this->objectManager,
'property' => 'name',
'is_method' => true,
'empty_option' => '-- none --',
'target_class' => 'YrmUser\Entity\Role',
'is_method' => true,
'find_method' => array(
'name' => 'findBy',
'params' => array(
'criteria' => array('parent' => null),
),
),
),
)
);
}
}
The hydration for the select in the form works as it only shows other roles that don't have a parent.
But when editing a existing entity it shows itself in the select so i can select itself as its parent.
I figured if i would have the id of current entity inside the form i can create a custom repo with a method that retrieves all roles without a parent and does not have the current entity id.
But i cant figure out how to get the id of the currently edited entity from inside the form.
Any help is appreciated.
Cheers,
Yrm
You can fetch the bound entity within the form using $this->getObject().
You have actually already set this with setObject(new Role());. Unfortunately this means that it was not loaded via Doctine and you will have the same issue, no $id to work with.
Therefore you will need to add the 'parent role' options (value_options) after you have bound the role loaded via doctrine.
From within the controller, I normally request the 'edit' form from a service class and pass in the entity instance or id that is being edited. Once set you can then modify existing form elements before passing it back to the controller.
// Controller
class RoleController
{
public function editAction()
{
$id = $this->params('id'); // assumed id passed as param
$service = $this->getRoleService();
$form = $service->getRoleEditForm($id); // Pass the id into the getter
// rest of the controller...
}
}
By passing in the $id when you fetch the form you can then, within a service, modify the form elements for that specific role.
class RoleService implements ObjectManagerAwareInterface, ServiceLocatorAwareInterface
{
protected function loadParentRolesWithoutThisRole(Role $role);
public function getRoleEditForm($id)
{
$form = $this->getServiceLocator()->get('Role\Form\RoleEditForm');
if ($id) {
$role = $this->getObjectManager()->find('Role', $id);
$form->bind($role); // calls $form->setObject() internally
// Now the correct entity is attached to the form
// Load the roles excluding the current
$roles = $this->loadParentRolesWithoutThisRole($role);
// Find the parent select element and set the options
$form->get('parent')->setValueOptions($roles);
}
// Pass form back to the controller
return $form;
}
}
By loading the options after the form has initialized you do not need the current DoctrineModule\Form\Element\ObjectSelect. A normal Select element that has no default value_options defined should be fine.