So, we have two entities. One with repository and another is not. When we trying to get the data from another table we will get the ArrayCollection data. Question is how to call this entity repository methods? Is it real?
Example:
$system = $this
->getDoctrine()
->getEntityManager()
->getRepository('SomeBundle:FirstEntity')
->findOneByColumnID($id);
$den = $system->getDataFromSecondTable(); // ArrayCollection of SecondEntity
And then i want to use some kind of:
$den[0]->functionFromSecondEntityRepository();
So, method "functionFromSecondEntityRepository" is in Repository of class SecondEntity and i can't call it - error on undefined method call "functionFromSecondEntityRepository".
So how can i do it in right way?
You didnt provide too many details so I will make some example up here.
Let's say you have an Entity FriendsList and a One-to-Many relationship with Entity Friend.
$List = $this->getDoctrine()
->getEntityManager()
->getRepository('SomeBundle:FriendsList')
->find($id);
// The list you pulled in by ID can now be used
$List->getId();
foreach($List->getFriends() as $Friend)
{
// Each friend will be output here, you have access
// to the Friend methods now for each.
$Friend->getId();
$Friend->getFirstName();
$Friend->getLastName();
$Friend->getDOB();
$Friend->getFavoriteColor();
}
By default when you create relationships a method to acquire the collection is created, in this example getFriends which returns an array of Entities. After you generate the entities look at your Entity Model to see which methods are available. By default one is created for each property in your entity and additional ones for Collections.
SomeCool/Bundle/Entity/FriendsList
Somecool/Bundle/Entity/Friend
The following is what a one-to-many relationship would look like if you use YAML configuration.
SomeCool\Bundle\Entity\FriendsList:
type: entity
table: null
oneToMany:
friend:
targetEntity: Friend
mappedBy: friendslist
cascade: ["persist"]
SomeCool/Bundle/Entity/Friend
manytoOne:
friends:
targetEntity: FriendsList
mappedBy: friend
cascade: ["persist"]
Accessing a Repository
YAML Configuration (services.yml)
somebundle.bundle.model.friends:
class: SomeBundle/Bundle/Model/Friends
arguments: [#doctrine.orm.entity_manager]
On the Controller
$friendsModel = $this->get('somebundle.bundle.model.friends');
$Friends = $friendsModel->findByFirstName('Bobby');
foreach($Friends as $Friend)
{
$Friend->getLastName();
}
Repository methods are not available in Entities. You would need a function in your AnotherEntity to grab the ArrayCollection. IE:
class FirstEntity {
public function getAnotherEntity()
{
return $this->anotherEntity;
}
}
class AnotherEntity
{
public function getArrayCollection()
{
return $this->myArrayCollection;
}
}
$firstEntity->getAnotherEntity()->getArrayCollection();
Another option would be to get the AnotherEntity's repository based on results from first:
$system = $this
->getDoctrine()
->getEntityManager()
->getRepository('SomeBundle:SomeEntity')
->findOneByColumnID($id);
$anotherEntity = $system->getAnotherEntity();
$anotherEntityResult = $this->getDoctrine()
->getRepository(get_class($anotherEntity))
->functionFromAnotherEntityRepository($anotherEntity->getId());
If using the second solution, I'd make sure that $anotherEntity is not null before attempting to retrieve the repository.
Related
There is such a structure:
There is a model book (Book) and model systems age restrictions (Rars).
One book can be only one rars, but on one rars can refer a lot of books. That is, the relationship - one to many?
The model Book:
class Book extends Model
{
public function rars()
{
return $this->belongsTo('App\Rars');
}
}
The model Rars:
class Rars extends Model
{
public function books()
{
return $this->hasMany('App\Book');
}
}
In migration Book:
$table->integer('rars_id');
$table->foreign('rars_id')->references('id')->on('rars');
Run code:
$book->rars()->save(\App\Rars::where('eternal_name', 'no_limits')->first());
(Rars with this eternal_name, guaranteed to exist)
And this return:
[BadMethodCallException]
Call to undefined method Illuminate\Database\Query\Builder::save()
What am I doing wrong?
According to the official documentation, for updating 'Belongs To' relationships you should use associate method.
So i think this will work:
$book->rars()->associate(\App\Rars::where('eternal_name', 'no_limits')->first());
$book->save();
For more information you can read here, https://laravel.com/docs/5.1/eloquent-relationships#inserting-related-models
Do not try to edit Rars with an Instance of Book. Make an instance of Rars instead. if you have the book id, do as following.
$book = new Book();
$book = $book->find($bookId);
$rars = new Rars();
$rars = $rars->find($book->rars_id);
/* Update data */
$rars->save();
There is often the case where an certain eloquent model's relation is unset (i.e. in a books table, author_id is null) and thus calling something like $model->relation returns null.
E.g. say a Book model has an author() (hasOne) relation I might want to do
$author = Book::find(1)->author->name;
If Book 1 has no author set it will throw a "trying to get property of non object" error. Is there a way to avoid this and default to a blank Author so I'll always be able to call name on it regardless of whether the relation has been set for the specific model?
Essentially I want to avoid conditionals to check if $book->author is an actual Author before calling further methods/properties on it. It should default to a new Author instance if the relation isn't set.
I tried something like:
public function getAuthorAttribute($author)
{
return $author ?: new Author;
}
however this doesn't work; $author is being passed in as null, even if it's set on the model. Presumably because it's a relation rather than a direct property of a book. I'd need something like
public function getAuthorAttribute()
{
return $this->author()->first() ?: new Author;
}
which seems pretty inelegant and seems like it would override any eager loading resulting in poor performance.
Update
As of Laravel 5.3.23, there is now a built in way to accomplish this (at least for HasOne relationships). A withDefault() method was added to the HasOne relationship. In the case of your Book/Author example, your code would look like:
public function author() {
return $this->hasOne(Author::class)->withDefault();
}
This relationship will now return a fairly empty (keys are set) Author model if no record is found in the database. Additionally, you can pass in an array of attributes if you'd like to populate your empty model with some extra data, or you can pass in a Closure that returns what you'd like to have your default set to (doesn't have to be an Author model).
Until this makes it into the documentation one day, for more information you can check out the pull requests related to the change: 16198 and 16382.
At the time of this writing, this has only been implemented for the HasOne relationship. It may eventually migrate to the BelongsTo, MorphOne, and MorphTo relationships, but I can't say for sure.
Original
There's no built in way that I know of to do this, but there are a couple workarounds.
Using an Accessor
The problem with using an accessor, as you've found out, is that the $value passed to the accessor will always be null, since it is populated from the array of attributes on the model. This array of attributes does not include relationships, whether they're already loaded or not.
If you want to attempt to solve this with an accessor, you would just ignore whatever value is passed in, and check the relationship yourself.
public function getAuthorAttribute($value)
{
$key = 'author';
/**
* If the relationship is already loaded, get the value. Otherwise, attempt
* to load the value from the relationship method. This will also set the
* key in $this->relations so that subsequent calls will find the key.
*/
if (array_key_exists($key, $this->relations)) {
$value = $this->relations[$key];
} elseif (method_exists($this, $key)) {
$value = $this->getRelationshipFromMethod($key);
}
$value = $value ?: new Author();
/**
* This line is optional. Do you want to set the relationship value to be
* the new Author, or do you want to keep it null? Think of what you'd
* want in your toArray/toJson output...
*/
$this->setRelation($key, $value);
return $value;
}
Now, the problem with doing this in the accessor is that you need to define an accessor for every hasOne/belongsTo relationship on every model.
A second, smaller, issue is that the accessor is only used when accessing the attribute. So, for example, if you were to eager load the relationship, and then dd() or toArray/toJson the model, it would still show null for the relatioinship, instead of an empty Author.
Overriding Model Methods
A second option, instead of using attribute accessors, would be to override some methods on the Model. This solves both of the problems with using an attribute accessor.
You can create your own base Model class that extends the Laravel Model and overrides these methods, and then all of your other models will extend your base Model class, instead of Laravel's Model class.
To handle eager loaded relationships, you would need to override the setRelation() method. If using Laravel >= 5.2.30, this will also handle lazy loaded relationships. If using Laravel < 5.2.30, you will also need to override the getRelationshipFromMethod() method for lazy loaded relationships.
MyModel.php
class MyModel extends Model
{
/**
* Handle eager loaded relationships. Call chain:
* Model::with() => Builder::with(): sets builder eager loads
* Model::get() => Builder::get() => Builder::eagerLoadRelations() => Builder::loadRelation()
* =>Relation::initRelation() => Model::setRelation()
* =>Relation::match() =>Relation::matchOneOrMany() => Model::setRelation()
*/
public function setRelation($relation, $value)
{
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When attempting
* to set to null, override with a new instance of the expected model.
*/
if (is_null($value)) {
// set the value to a new instance of the related model
$value = $this->$relation()->getRelated()->newInstance();
}
$this->relations[$relation] = $value;
return $this;
}
/**
* This override is only needed in Laravel < 5.2.30. In Laravel
* >= 5.2.30, this method calls the setRelation method, which
* is already overridden and contains our logic above.
*
* Handle lazy loaded relationships. Call chain:
* Model::__get() => Model::getAttribute() => Model::getRelationshipFromMethod();
*/
protected function getRelationshipFromMethod($method)
{
$results = parent::getRelationshipFromMethod($method);
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When the
* result is null, override with a new instance of the related model.
*/
if (is_null($results)) {
$results = $this->$method()->getRelated()->newInstance();
}
return $this->relations[$method] = $results;
}
}
Book.php
class Book extends MyModel
{
//
}
I had the same problem in my project. In my views there's some rows that are accesing to dinamics properties from null relationships, but instead of returning an empty field, the app was thrwoing and exception.
I just added a foreach loop in my controller as a temporal solution that verifies in every value of the collection if the relationship is null. If this case is true, it assigns a new instance of the desire model to that value.
foreach ($shifts as $shift)
{
if (is_null($shift->productivity)) {
$shift->productivity = new Productivity();
}
}
This way when I access to $this->productivity->something in my view when the relationship is unset, I get a empty value instead of an exception without putting any logic in my views nor overriding methods.
Waiting for a better solution to do this automatically.
You can achieve this using model factories.
Define an author factory inside your ModelFactory.php
$factory->define(App\Author::class, function (Faker\Generator $faker) {
return [
'name' => $faker->firstName, //or null
'avatar' => $faker->imageUrl() //or null
];
});
add values for all the needed attributes I am using dummy values from Faker but you can use anything you want.
Then inside your book model you can return an instance of Author like this:
public function getAuthorAttribute($author)
{
return $author ?: factory(App\Author::class)->make();
}
I can add record to many to many table tag_post successfully but I'm not able to remove any records from tag_post. Please bear in mind I only want to remove records from tag_post not the record from table post itself.
I have 3 tables post,tag and tag_post. tables tag_post contains relation between post and tag. the fields in table tag_post are :
tag_id
post_id
Mapping file for post:
oneToMany:
tagPostAssociations:
targetEntity: Mockizart\Bundle\BlogBundle\Entity\MockblogTagPost
mappedBy: "post"
cascade: ["persist","remove"]
Mapping file for tag:
oneToMany:
tagPostAssociations:
targetEntity: Mockizart\Bundle\BlogBundle\Entity\MockblogTagPost
mappedBy: "tag"
cascade: ["persist","remove"]
Mapping file for tag_post:
manyToOne:
post:
associationKey: true
targetEntity: Mockizart\Bundle\BlogBundle\Entity\MockblogPost
inversedBy: "tagPostAssociations"
tag:
targetEntity: Mockizart\Bundle\BlogBundle\Entity\MockblogTag
inversedBy: "tagPostAssociations"
My code for test:
$post = $this->getDoctrine()->getManager()->find('MockizartBlogBundle:MockblogPost',6);
$b = $this->getDoctrine()->getManager()->find('MockizartBlogBundle:MockblogTagPost',['tagId' => 20,'postId' => 6]);
$post->removeTagPostAssociation($b);
$this->getDoctrine()->getManager()->persist($post);
$this->getDoctrine()->getManager()->flush();
My tag_post entity:
public function __construct($tag, $post)
{
$this->tagId = $tag->getId();
$this->postId = $post->getId();
$this->post = $post;
$this->tag = $tag;
}
My post entity:
public $tagPostAssociations;
public function __construct() {
$this->tagPostAssociations = new ArrayCollection();
}
public function addTagPostAssociation(MockblogTagPost $tagPostAssociations)
{
$newTag = $tagPostAssociations;
$this->newTags[$newTag->getTagId().$newTag->getPostId()] = $newTag;
$hasTagPost = $this->hasTagPost($newTag);
if (!$hasTagPost) {
$this->tagPostAssociations[] = $tagPostAssociations;
}
return $this;
}
public function removeTagPostAssociation(MockblogTagPost $tagPost)
{
$this->tagPostAssociations->removeElement($tagPost);
return $this;
}
public function getTagPostAssociations()
{
return $this->tagPostAssociations;
}
I only post codes that I think related to the case. if you want to see more code, please let me know.
I can't test your setup, however I believe you've mixed up your understanding of the cascade attribute with Doctrine. See, if you did something like:
$post = $this->getDoctrine()->getManager()->find('MockizartBlogBundle:MockblogPost',6);
$this->getDoctrine()->getManager()->remove($post);
$this->getDoctrine()->getManager()->flush();
Then Doctrine would recognize that you are deleting the $post in question and would cascade operations to all of the associations that have the "remove" cascade rule defined, which would delete the tag_post entry in question. However you're not deleting your $post, so you must manually remove the tag_post after taking it out of your object:
$post = $this->getDoctrine()->getManager()->find('MockizartBlogBundle:MockblogPost',6);
$b = $this->getDoctrine()->getManager()->find('MockizartBlogBundle:MockblogTagPost',['tagId' => 20,'postId' => 6]);
$post->removeTagPostAssociation($b);
// This may need to come after the persist, haven't tested
$this->getDoctrine()->getManager()->remove($b);
$this->getDoctrine()->getManager()->persist($post);
$this->getDoctrine()->getManager()->flush();
This is obviously cumbersome if you're constantly removing entries, so perhaps an event listener could automate this process for you.
One other thing to note is that you may be able to get away with just removing the tag_post and not touching your post or tag objects at all:
$b = $this->getDoctrine()->getManager()->find('MockizartBlogBundle:MockblogTagPost',['tagId' => 20,'postId' => 6]);
$this->getDoctrine()->getManager()->remove($b);
$this->getDoctrine()->getManager()->flush();
Reason being is that all of the foreign reference columns "live" in the tag_post table, meaning there would be no integrity violations by simply removing a ManyToMany reference. Only thing is any loaded post or tag object would have out-of-date tagPostAssociations
I have 3 "main" entities : TypeA and TypeB linked to User by a ManyToOne relation.
I have 2 "secondary" entities : UserTypeA and UserTypeB, which contain the attributes of the ManyToOne relations (for example, the comment a user has assigned to a product of type A). These two entities and their repository are similar (except that one is linked to TypeA and the other to TypeB).
Here is a part of my code :
public function typea_commentAction(TypeA $typea)
{
$user = $this->getUser();
$userTypeA = $this->getDoctrine()
->getManager()
->getRepository('GamUserBundle:UserTypeA')
->getComment($user, $typea);
//...
}
public function typeb_commentAction(TypeB $typeb)
{
$user = $this->getUser();
$userTypeB = $this->getDoctrine()
->getManager()
->getRepository('GamUserBundle:UserTypeB')
->getComment($user, $typeb);
//...
}
As you can see, I need to duplicate each action to make them work with each entity. Is there any way to combine these actions ? Same question about the secondary entities and their repositories.
Thanks.
Create a service class that performs the logic and takes say, the user type as a parameter.
Is there a method in Doctrine like Hibernate's findByExample method?
thanks
You can use the findBy method, which is inherited and is present in all repositories.
Example:
$criteria = array('name' => 'someValue', 'status' => 'enabled');
$result = $em->getRepository('SomeEntity')->findBy($criteria);
You can create findByExample method in one of your repositories using a definition like this:
class MyRepository extends Doctrine\ORM\EntityRepository {
public function findByExample(MyEntity $entity) {
return $this->findBy($entity->toArray());
}
}
In order for this to work, you will have to create your own base class for the entities, implementing the toArray method.
MyEntity can also be an interface, which your specific entities will have to implement the toArray method again.
To make this available in all your repositories, ensure that you are extending your base repository class - in this example, the MyRepository one.
P.S I assume you are talking about Doctrine 2.x
Yes.
Let's say you have a model called Users. You have the following two classes
abstract class Base_User extends Doctrine_Record
{
//define table, columns, etc
}
class User extends Base_User
{
}
in some other object you can do
$user = new User;
//This will return a Doctrine Collection of all users with first name = Travis
$user->getTable()->findByFirstName("Travis");
//The above code is actually an alias for this function call
$user->getTable()->findBy("first_name", "Travis");
//This will return a Doctrine Record for the user with id = 24
$user->getTable()->find(24);
//This will return a Doctrine Collection for all users with name=Raphael and
//type = developer
$user->getTable()
->findByDql("User.name= ? AND User.type = ?", array("Raphael", "developer"));
$users = $userTable->findByIsAdminAndIsModeratorOrIsSuperAdmin(true, true, true);
See http://www.doctrine-project.org/projects/orm/1.2/docs/manual/dql-doctrine-query-language/en