Doctrine – ArrayCollection->add() throws an exception

Notice: This thread is very old.
Čamo
Member | 786
+
0
-

Hello,
Can you help me please find out where is the problem? ArrayCollection->add() throws me an exception: “Call to a member function add() on array”.

I have created entity like this:

<?php

namespace App\Model\Entity;

use Doctrine\ORM\Mapping as ORM;
use Kdyby;
use App;


/**
 * @ORM\Entity(repositoryClass="App\Model\Repository\ArticleRepository")
 * @ORM\Table(options={"collate"="utf8_slovak_ci"})
 */
class Article extends Kdyby\Doctrine\Entities\BaseEntity
{

	use Kdyby\Doctrine\Entities\Attributes\Identifier;


	public function __construct()
	{
		$this->tags = new \Doctrine\Common\Collections\ArrayCollection();
	}

	/**
	 * @ORM\manyToMany(targetEntity="Tag", inversedBy="articles")
	 * @ORM\joinTable(
	 * 		name="article_tag",
	 * 		joinColumns={
	 * 			@ORM\joinColumn(name="article_id", referencedColumnName="id")
	 * 		},
	 * 		inverseJoinColumns={
	 * 			@ORM\joinColumn(name="tag_id", referencedColumnName="id")
	 * 		}
	 * )
	 */
	protected $tags;

	...

but if I try use it this way:

		$tags = $tagRepository->findBy( [ 'id' => [ 9, 8, 7 ] ] );
		$user = $userRepository->find( 1 );

		$article = new Model\Entity\Article();

		$date = date( 'j.n.Y H:i:s' );

		$article->title = 'Some title ' . $date;
		$article->content = 'Some content ' . $date;
		$article->user = $user;
		$article->tags->add( $tags );

		$this->em->persist( $article );
		$this->em->flush();

it throws me an exception: “Call to a member function add() on array”
May be the problem is that I added __constructor later and something is wrong with cache. But console orm:schema-tool:update writes: “Nothing to update …”

Thank you.

EDIT:
As I found out correct syntax is

foreach ( $tags as $tag )
{
	$article->tags->add( $tag );
}

but it still throws the same exception.

Last edited by Čamo (2015-10-21 21:57)

enumag
Member | 2118
+
0
-

Read the error message again. Carefully.

Čamo
Member | 786
+
0
-

As I found out it should be:

foreach ( $tags as $tag )
{
	$article->tags->add( $tag );
}

but it still throws the same exception.

Azathoth
Member | 495
+
+1
-

This is propably because of magic getters of BaseEntity. Try to generate getter for tags property, which returns collection. But you should not use collection outside entity, so the add method should be called inside entity.

Čamo
Member | 786
+
0
-

Azathoth:
Base entity __get() has this implementation:

		...
		// protected attribute support
		$properties = $this->listObjectProperties();
		if (isset($properties[$name = func_get_arg(0)])) {
			if ($this->$name instanceof Collection) {
				$coll = $this->$name->toArray();

				return $coll;

			} else {
				$val = $this->$name;

				return $val;
			}
		}
		...

so it should return array. Also as I found out the implementation of __call enables to use

foreach ( $tags as $tag )
{
	$article->addTag( $tag );
}

which works for me. But as I try to solve it I created new Test entity with tags collection and it works also with $test->tags->add( $tag ). So I am confused.
EDIT: Test entity works cause it has getTags() method. That was my mistake.

Last edited by Čamo (2015-10-21 22:48)

Čamo
Member | 786
+
0
-

Azathoth:
You are right. If I define getter it works fine. Also my Test entity has getTags method so it works as expected.
Thank you very much!

Last edited by Čamo (2015-10-21 22:49)

Filip Procházka
Moderator | 4668
+
0
-

Be aware, that using getters that return and expose raw collections is a bad practice! That's why Kdyby returns only the Array.

If instead of extending BaseEntity, you use MagicAccessors, you'll get the same functionality, but instead of returning arrays it returns readonly collections. Which doesn't solve your problem, I know :) But BaseEntity has been deprecated.

If you wanna use magic, then instead of

$article->tags->add($tag);

use

$article->addTag($tag);

You can also disable the magic simply by declaring the field private. Which means you'd have to define the method yourself.

public function addTag(Tag $tag) {
    $this->tags->add($tag);
}

Anyway, you should not expose tha raw collection, it's bad.

Čamo
Member | 786
+
0
-

“Be aware, that using getters that return and expose raw collections is a bad practice!”
You are right.
“BaseEntity has been deprecated.”
Do you mean class MyEntity extends BaseEntity at all?

It is little weird. Php use public/protected/private to solve encapsulation. Properties of Doctrine Entities has to be min protected. I don't understand that. Of course I am totally newbie. But that setters and getters make huge objects form every entity. It looks like a haystack :|.

Why Doctrine can't simply check (if I use $prop->add()) type hint and if property is public – which simply means I want to add?

Have a nice day.

Last edited by Čamo (2015-10-23 21:41)

Azathoth
Member | 495
+
+1
-

There is no reason to have public properties. It breaks encapsulation.

So all properties should be private or protected.
And you also should not use collections (collections as object properties) outside of entities. Because of encapsulation.
When you use public properties, entity has no control which value it has. Thus, entity could be in invalid state.
NEVER let your entities be in invalid state.
In setter you can validate (whether it is string, bool, integer, phone number, email, whatever). In public property, you can not validate.

And do not generate all getters and setters immediately when creating new class.
Think, whether you need this getter/settier.
It is bad practise to generate all setters.
Of course, some times you need setter for every property. But when you do not need CRUD, do not use CRUD.
This article can offer you another point of view on entities. This article is also very useful.

And your entities should be valid and should work even if you wont be using Doctrine.

Čamo
Member | 786
+
0
-

Thanks.