I am trying to embed collection of Tag forms to Service form, according to this tutorial. Tag and Service entities have many-to-many relationship.
Form is rendering correctly. But when I submit form, I get
Could not determine access type for property "tagList"
error. I don't understand why new Tag object is not added to the Service class by calling the addTag() method.
ServiceType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, array(
'label' => 'Title'
))
;
$builder->add('tagList', CollectionType::class, array(
'entry_type' => TagType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
)));
}
Service class
{
....
/**
* #ORM\ManyToMany(targetEntity="Tag", mappedBy="serviceList",cascade={"persist"})
*/
private $tagList;
/**
* #return ArrayCollection
*/
public function getTagList()
{
return $this->tagList;
}
/**
* #param Tag $tag
* #return Service
*/
public function addTag(Tag $tag)
{
if ($this->tagList->contains($tag) == false) {
$this->tagList->add($tag);
$tag->addService($this);
}
}
/**
* #param Tag $tag
* #return Service
*/
public function removeTag(Tag $tag)
{
if ($this->tagList->contains($tag)) {
$this->tagList->removeElement($tag);
$tag->removeService($this);
}
return $this;
}
}
Tag class
{
/**
* #ORM\ManyToMany(targetEntity="Service", inversedBy="tagList")
* #ORM\JoinTable(name="tags_services")
*/
private $serviceList;
/**
* #param Service $service
* #return Tag
*/
public function addService(Service $service)
{
if ($this->serviceList->contains($service) == false) {
$this->serviceList->add($service);
$service->addTag($this);
}
return $this;
}
/**
* #param Service $service
* #return Tag
*/
public function removeService(Service $service)
{
if ($this->serviceList->contains($service)) {
$this->serviceList->removeElement($service);
$service->removeTag($this);
}
return $this;
}
}
ServiceController
public function newAction(Request $request)
{
$service = new Service();
$form = $this->createForm('AppBundle\Form\ServiceType', $service);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($service);
$em->flush();
return $this->redirectToRoute('service_show', array('id' => $service->getId()));
}
return $this->render('AppBundle:Service:new.html.twig', array(
'service' => $service,
'form' => $form->createView(),
));
}
Could you please try to implement code from this URL?
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#owning-and-inverse-side-on-a-manytomany-association
First, please try to change mapped/inverse sides, and remove $service->addTag($this); from Tag::addService method.
Short version:
I just ran into this problem and solved it by adding a setter for the affected property:
Could not determine access type for property "tagList"
public function setTagList(Array $tagList)
{
$this->tagList = $tagList;
}
Long version:
The error message is signaling that Symfony is trying to modify the object's state, but cannot figure out how to actually make the change due to the way its class is set up.
Taking a look at Symfony's internals, we can see that Symfony gives you 5 chances to give it access and picks the best one in this order from top to bottom:
A setter method named setProperty() with one argument:
This is the first thing Symfony checks for and is the most explicit way to achieve this. As far as I'm aware this is the best practice:
class Entity {
protected $tagList;
//...
public function getTagList()
{
return $this->tagList;
}
//...
}
A combined getter and setter in one method with one argument:
It's important to realize that this method will also be accessed by Symfony in order to get the object's state. Since those method calls don't include an argument, the argument in this method must be optional.
class Entity {
protected $tagList;
//...
public function tagList($tags = null)
{
if($reps){
$this->tagList = $tags;
} else {
return $this->tagList;
}
}
//...
}
The affected property being declared as public:
class Entity {
public $tagList;
//... other properties here
}
A __set magic method:
This will affect all properties rather than just the one you intended.
class Entity {
public $tagList;
//...
public function __set($name, $value){
$this->$name = $value;
}
//...
}
A __call magic method (in some cases):
I wasn't able to confirm this, but the internal code suggests this is possible when magic is enabled on PropertyAccessor's construction.
Only using one of the above strategies is required.
Maybe the problem is that Symfony can't access that property?
If you look at where that exception is thrown (writeProperty method in the PropertyAccessor class) it says it can be thrown:
If the property does not exist or is not public.
In the tutorial you mentioned it has property $tags, and method addTag. I'm just guessing here, but maybe there's a convention where it tries to call a method names add($singularForm) and this is failing for you because the property is tagList and the method is addTag.
I'm not 100% sure, but you could try debugging by setting a stop point in that Symfony method to see why it's being thrown.
Maybe you forgot in the __construct() of Service class and Tag class to initialize $tagList and $serviceList like this ?
$this->tagList = new ArrayCollection();
$this->serviceList = new ArrayCollection();
This seems like an error with your constructor. Try this :
public function __construct()
{
$this-> tagList = new \Doctrine\Common\Collections\ArrayCollection();
}
It's a long shot, but looking at your annotations I think the problem might be related to your manyToMany relationship. Try to change the owning side and inverse side (Swap the relationship) unless you specifically need to update from both ends (In that case I think the only solution is to add the objects manually or use oneToMany relationships).
Changes made only to the inverse side of an association are ignored.
Make sure to update both sides of a bidirectional association (or at
least the owning side, from Doctrine’s point of view)
This is a problem related to Doctrine I have suffered before, see:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/unitofwork-associations.html
Based on Symfony 3.3.10
I actually faced this problem many and many times, finally once i discovered where this problem was coming from, depending on the name you give to your entity property it can happen that the adder and the remover for your collection property aren't exactly what you are expecting.
Example: Your entity properity name is "foo" and you would expect the adder to be called "addFoo" and remover "removeFoo", but then all of a sudden the "Could not determine access type for property" appear.
So you start going into fear searching for w/e problems in your code, instead you just have to look this file inside Symfony core files:
vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
Inside this file there's a method called findAdderAndRemover.
Go there with your debugger and you will eventually find out that symfony searches for weird name for your adder/remover, they may actually end with "um" or "on" or "us" depending on the language (human language) you used to name them. Since i'm Italian this happen quite often.
Watch out for that, since the fix may be as simple as changing the name used for your add/remove method inside your entity to make them match with what Symfony core is looking for.
This happens to me when i use bin/console doctrine:generate:entities to create the methods automatically for me
If you are using symfony, and use EntityRepository instead of CollectionType, make sure you use the 'multiple' => true, on your form build, otherwise the input will be for one entity and not for many, therefore it will call the setTagList instead of using the methods addTagList and removeTagList.
I am setting up new project based on Yii2 and Codeception. I am using advanced app template (backend, common, frontend).
I want most of frontend ActiveRecords to be "ReadOnly" so I made special Trait where I am blocking appropriate methods like save, insert, delete, update, ...
trait ReadOnlyActiveRecord {
/**
* #throws \yii\web\MethodNotAllowedHttpException
*/
public function save($runValidation = true, $attributeNames = null)
{
return self::throwNotAllowedException(__FUNCTION__);
}
//...
It simply throws an MethodNotAllowedHttpException.
Now I use this Trait in multiple frontend ARs and want to test them using Codeception like following
use \Codeception\Specify;
/**
* #expectedException \yii\web\MethodNotAllowedHttpException
*/
public function testSaveMethod()
{
$model = new AppLanguage();
$this->specify('model should be unsaveable', function () use ($model) {
expect('save function is not allowed', $model->save())->false();
});
}
/**
* #expectedException \yii\web\MethodNotAllowedHttpException
*/
public function testInsertMethod()
{
$model = new AppLanguage();
$this->specify('model should be unisertable', function () use ($model) {
expect('insert function is not allowed', $model->save())->false();
});
}
// ...
I now I am figuring out how to use these tests in multiple TestCests so I won't rewrite the code again and again in each of Cest.
So I am thinking about something like
/**
* Tests ActiveRecord is read only
*/
public function testReadOnly()
{
$model = new AppLanguage();
$this->processReadOnlyTests($model);
}
So my question is:
Where to put the test methods and how to include and call them in specific Cests?
Any suggestions?
Thank you.
I'm trying to figure out a way to cascade an operation on all entities associated with an entity. For example, if I have a User, and a user has entities, and those entities have entities, I want to perform an operation on every entity. Specifically I want to perform validation on every entity in that tree.
How would I implement something like 'getAssociatedEntities()':
class User {
/**
* #ManyToOne(targetEntity="Comment")
*/
private $comment;
/**
* #ManyToOne(targetEntity="Something")
*/
private $something;
}
$user->setComment($comment);
$user->setSomething($something);
$associated_entities = $user->getAssociatedEntities(); // NOT A REAL METHOD
foreach ($associated_entities AS $entity) {
validate($entity);
}
I realize I could use lifecycle callbacks to perform validation. However, setting validation annotations makes things sooooo much easier. I can validate every entity I persist, BUT I can't validate its associated entities.
I can validate every entity I persist, but I can't validate its
associated entities.
Of course you can perform any action to entitied associated with main entity if:
You add cascade={"all"} to #ManyToOne definition (required !).
#HasLifecycleCallbacks for each sub-entity you want action performed and mark methods #PrePersist or #PostPersist or more in documentation.
For example I use this method to cascade deletion of image file in the Image entity, while the any entity that contains Image entity is beeing deleted:
Here I have an livecycle callback:
/**
* An image.
*
* #ORM\Entity
* #ORM\Table(name="images")
* #ORM\HasLifecycleCallbacks
*/
class ImageEntity extends AbstractEntity
{
/**
* #ORM\PreRemove
*/
public function preRemoveDeleteFile()
{
// remoce file
}
}
Here I have an entity that contains Image (where there is a cascade operation defined):
/**
* A competency group name.
*
* #ORM\Entity
* #ORM\Table(name="product_meta_image_cover")
*/
class ProductMetaImageCoverEntity extends AbstractEntity
{
/**
* #var \ModuleModel\Entity\ImageEntity
* #ORM\OneToOne(targetEntity="ModuleModel\Entity\ImageEntity", cascade={"all"}, orphanRemoval=true)
* #ORM\JoinColumn(onDelete="CASCADE")
*/
protected $image;
}
I'm doing test unit of my entity:
namespace PathtomyBundle\Tests;
require_once dirname(__DIR__).'/../../../app/AppKernel.php';
use Doctrine\ORM\Tools\SchemaTool;
abstract class TestCase extends \PHPUnit_Framework_TestCase
{
/**
* #var Symfony\Component\HttpKernel\AppKernel
*/
protected $kernel;
/**
* #var Doctrine\ORM\EntityManager
*/
protected $entityManager;
/**
* #var Symfony\Component\DependencyInjection\Container
*/
protected $container;
public function setUp()
{
// Boot the AppKernel in the test environment and with the debug.
$this->kernel = new \AppKernel('test', true);
$this->kernel->boot();
// Store the container and the entity manager in test case properties
$this->container = $this->kernel->getContainer();
$this->entityManager = $this->container->get('doctrine')->getEntityManager();
// Build the schema for sqlite
//$this->generateSchema();
parent::setUp();
}
public function tearDown()
{
// Shutdown the kernel.
$this->kernel->shutdown();
parent::tearDown();
}
protected function generateSchema()
{
// Get the metadatas of the application to create the schema.
$metadatas = $this->getMetadatas();
if ( ! empty($metadatas)) {
// Create SchemaTool
$tool = new SchemaTool($this->entityManager);
$tool->createSchema($metadatas);
} else {
throw new Doctrine\DBAL\Schema\SchemaException('No Metadata Classes to process.');
}
}
/**
* Overwrite this method to get specific metadatas.
*
* #return Array
*/
protected function getMetadatas()
{
return $this->entityManager->getMetadataFactory()->getAllMetadata();
}
}
and also:
namespace pathtomybundle\Tests\Entity;
use pathtomybundle\Tests\TestCase;
use pathtomybundle\Entity\Calendars;
require_once dirname(__DIR__).'/TestCase.php';
class CalendarsDbTest extends TestCase
{
protected $Calendars;
public function setUp()
{
parent::setUp();
$this->Calendars = new Calendars();
}
public function testGenerateCalendars()
{
$this->Calendars->setBeginDate(new \DateTime('now'));
$this->Calendars->setDescription('Description');
$this->Calendars->setEndDate(new \DateTime('now'));
$this->Calendars->setType('sur titre');
// Save the ExCalendars
$this->entityManager->persist($this->Calendars);
$this->entityManager->flush();
}
public function testUser(){
$this->assertEquals('Description', $this->Calendars->getDescription() );
}
So my questions are:
Why does it raise this error "Failed asserting that null matches expected"?
Why getDescription() returns NULL?
How to test two table with One-to-Many relationship for example my Table Calendars with another table in database?
Edit
For the third question :
For example I have two Tables Job and Calenders with Many-to-One relationship so I will have a Job_Id field in Calendars Table,so how I will do my test Unit with a foreign key "job_id"
In Calendars Entity :
/**
* #var Job
*
* #ORM\ManyToOne(targetEntity="Job")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="job_id", referencedColumnName="job_id")
* })
*/
private $jobId;
Edit-2-
when I run my phpunit test "phpunit -c app" to test setters function and persist in database so I have a with every test a new data insered in databse, my question is it possible to do a lot of test but I insert data in database just for one time because actually I must remove data from database with every test.
2 - another question : to create a database_test i use "$this->generateSchema();
" so after create a database for the first time and when the test call "TestCase"class (the code above) again so he tried to create the database_test again then I must remove the line after the first time and it's not good,so what I can do to run this line just for one time in the first time when i run my test?
Edit-3
/**
* #var Job
*
* #ORM\ManyToOne(targetEntity="Job")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="job_id", referencedColumnName="id")
* })
*/
private $job;
it's normal?
Every test in test case creates his own CalendarsDbTest object. So, in fact, $this->Calendar is different object in each test (if you want share it between tests you need create it in setUp method)
Is the same as above (there is null because you never call setDescription with $this->Calendars - it's different object than it is in first test)
I'm not sure what exactly you mean. Can you show more precise (for example method you want test) what you mean?
edit:
The answer is: you don't test it. Why? Because unit test is UNIT test - you should test here only your entity. Persistence, keeping relations etc. are Doctrine resposibility and should be tested there - you don't worry about it.
The only thing you should test is setter/getter for $jobId property (btw. it should be "$job" rather than "$jobId" because it's object of Job class - not an integer), eg.:
class CalendarTest extends \PHPUnit_Framework_TestCase
{
(...)
public function testSetGetJob()
{
$job = new Job();
$job->setSomeProperty('someValue');
$expectedJob = clone $job; // you clone this because in setter you pass object by reference
$calendar = new Calendar();
$calendar->setJob($job);
$this->assertEquals($expectedJob, $calendar->getJob());
}
(...)
}
I have the following setup "Many Users can have Many Projects (Collaborators)"
/**
* #Entity #HasLifeCycleCallbacks
* #Table(name="projects")
*/
class Project implements \Zend_Acl_Resource_Interface {
/**
* #ManyToMany(targetEntity="User", mappedBy="projects")
* #OrderBy({"displayName" = "ASC", "username" = "ASC"})
*/
protected $collaborators;
..
}
/**
* #Entity
* #Table(name="users")
*/
class User implements \Zend_Acl_Role_Interface {
/**
* #ManyToMany(targetEntity="Project", inversedBy="collaborators")
*/
protected $projects;
...
}
I tried to remove a collaborator using the following
$user = Application_DAO_User::findById($this->_getParam('userid'));
$proj = Application_DAO_Project::getProjectById($this->_getParam('id'));
Application_DAO_Project::removeCollaborator($proj, $user); // <---
// Application_DAO_User
public static function findById($id) {
return self::getStaticEm()->find('Application\Models\User', $id);
}
// Application_DAO_Project
public static function getProjectById($id) {
return self::getStaticEm()->find('Application\Models\Project', $id);
}
public static function removeCollaborator(Project $proj, User $collaborator) { // <---
$proj->getCollaborators()->remove($collaborator);
$collaborator->getProjects()->remove($proj);
self::getStaticEm()->flush();
}
And there isn't any errors but the database stays the same ...
This may be well over due but was just experiencing the same problem myself... According to the doctrine 2 documents, the function ArrayCollection->remove($i) is for removing by array index.
What you are after is:
getCollaborators()->removeElement($collaborator);
I went round in circles trying to figure this out until I realised that for this to work:
getCollaborators()->removeElement($collaborator);
$collaborator would have to be the actual object from the collaborators ArrayCollection. That is, if you pass in a new Collaborator object with the same parameters it won't remove it. That's because ArrayCollection uses array_search to look for the object you want to remove.
Hope that saves someone else a few hours...