Doctrine 2 postPersist then save/update entity - php

I'm in the process of creating a trait that I want to insert into a number of my Doctrine entity classes. The trait basically allows for a slug property to be created using the Hashids PHP library based on the entities id (primary key).
I've included the required properties & getters/setters along with the postPersist() method on the trait, but I'm now wondering how I go about re-saving / updating / persisting that change from within the postPersist() method?
Any help or direction would be great.
SlugTrait
trait Slug
{
/**
* #ORM\Column(type="string")
*/
private $slug;
/**
* #ORM\PostPersist
*/
public function postPersist()
{
$this->slug = (new SlugCreator())->encode($this->id);
// Save/persist this newly created slug...?
}
public function getSlug()
{
return $this->slug;
}
public function setSlug($slug)
{
$this->slug = $slug;
}
}

After some trial and error I found out how to persist the update/change. As I'm using Laravel I just resolved the Entity Manager from the IoC container and then used that to persist the updated slug field like so (you could also just new up the Entity Manager manually):
trait Slug
{
/**
* #ORM\Column(type="string")
*/
protected $slug;
/**
* #ORM\PostPersist
*/
public function postPersist()
{
$this->slug = (new SlugCreator())->encode($this->id);
// Save/persist this newly created slug.
// Note: We must add the top level class annotation
// '#ORM\HasLifecycleCallbacks()' to any Entity that
// uses this trait.
$entityManager = App::make('Doctrine\ORM\EntityManagerInterface'); // or new up the em "new EntityManager(...);
$entityManager->persist($this);
$entityManager->flush();
}
public function getSlug()
{
return $this->slug;
}
public function setSlug($slug)
{
$this->slug = $slug;
}
}

Related

Symfony ManyToMany setter return wrong data

I have relation User to Coupon ManyToMany.
User have many coupons and coupon may belong to many users.
When I call the method $coupon->getUsers(), I get coupon (PersistentCollection).
And when I call the method $user->getCoupon(), I get user (PersistentCollection).
User entity:
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Coupon", inversedBy="users")
*/
private $coupon;
public function __construct()
{
$this->coupon = new ArrayCollection();
}
/**
* #return Collection|Coupon[]
*/
public function getCoupon(): Collection
{
return $this->coupon;
}
public function addCoupon(Coupon $coupon): self
{
if (!$this->coupon->contains($coupon)) {
$this->coupon[] = $coupon;
}
return $this;
}
public function removeCoupon(Coupon $coupon): self
{
if ($this->coupon->contains($coupon)) {
$this->coupon->removeElement($coupon);
}
return $this;
}
Coupon entity:
/**
* #ORM\ManyToMany(targetEntity="App\Entity\User", mappedBy="coupon")
*/
private $users;
public function __construct()
{
$this->users = new ArrayCollection();
}
/**
* #return Collection|User[]
*/
public function getUsers(): Collection
{
return $this->users;
}
public function addUser(User $user): self
{
if (!$this->users->contains($user)) {
$this->users[] = $user;
$user->addCoupon($this);
}
return $this;
}
public function removeUser(User $user): self
{
if ($this->users->contains($user)) {
$this->users->removeElement($user);
$user->removeCoupon($this);
}
return $this;
}
When I run this code:
namespace App\Controller;
use App\Entity\Coupon;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class TestController extends AbstractController
{
/**
* #Route("/test", name="test")
*/
public function index()
{
$coupon = $this->getDoctrine()->getRepository(Coupon::class)->find(1);
dump($coupon->getUsers());die;
}
}
I get:
screenshot
Why I get a coupon and not a list of users?
On top of what Jakumi wrote, in the controller you can also do
$coupon = $this->getDoctrine()->getRepository(Coupon::class)->find(1);
$users = $coupon->getUsers();
$users->initialize();
Now when you dump($users) the collection should not be empty.
To add to that, I believe you have your mapping wrong. In your Many-To-Many relation the User is the owning side and Coupon is the inversed side, however it is the public function addUser(User $user) in the Coupon entity that does the owning side's job. You should either change the sides (change the mappedBy in Coupon to inversedBy and the other way around in the User) or make sure that User does:
public function addCoupon(Coupon $coupon): self
{
if (!$this->coupon->contains($coupon)) {
$coupon->addUser($this);
$this->coupon[] = $coupon;
}
return $this;
}
and the Coupon does:
public function addUser(User $user): self
{
if (!$this->users->contains($user)) {
$this->users[] = $user;
}
return $this;
}
Of course the removeUser and removeCoupon methods should be dealth with accordingly.
PersistentCollections conceptually are supposed to work like arrays and are the way of doctrine to realize lazy loading (the default). There are certain operations that will trigger the collection to be loaded from the database (such as iterating over the collection). Before that, it's property initialized will be false (as in your screenshot)
ManyToMany and OneToMany should always be realized as ArrayCollection (or some other collection, such as PersistentCollection) and should not be leaked to the outside. Instead call ->toArray() (or ->asArray(), I always forget) to return them (so, inside getUsers() or getCoupons() respectively). Inside the entity you can just foreach over the PersistentCollection.
If you mark the ManyToMany to fetch as EAGER, it will be loaded immediately, but that might have performance impact...
And the Collection holds a reference to the object it belongs to, so you're not getting a Coupon per se, you get a collection, that still references its owner ;o)

PhpStorm metadata file for repository classes

In our application, we use repositories for models that are fetched from the database. So, we have an abstract repository that knows about the database, has a loadById method to load a database record and an abstract getEntity method that creates an object for that specific repository. Example code:
abstract class EntityRepository {
/**
* #param int $id
* #return AbstractEntity
*/
public function loadById($id) {
$record = $this->db->loadById($id);
$entity = $this->getEntity();
return $this->inflate($record, $entity);
}
/**
* #return AbstractEntity
*/
protected abstract function getEntity();
}
class PeopleRepository extends EntityRepository {
protected function getEntity() {
return new PeopleEntity();
}
}
abstract class AbstractEntity {
private $id;
/**
* #return int
*/
public function getId() {
return $this->id;
}
/**
* #param int $id;
*/
public function setId($id) {
$this->id = $id;
}
}
class PeopleEntity extends AbstractEntity {
private $name;
/**
* #return string
*/
public function getName() {
return $this->name;
}
/**
* #param string $name;
*/
public function setName($name) {
$this->name= $name;
}
}
When using an instance of PeopleRepository and fetching a model through loadById, PhpStorm is not able to resolve the returned model to a concrete type, but provides only code completion for the functions of AbstractEntity. Is there any simple way to make it work?
In https://confluence.jetbrains.com/display/PhpStorm/PhpStorm+Advanced+Metadata, I've only found ways to make it work for concrete classes and their functions. So, enumerating all repository classes and all their ways of creating an entity might work. But I'd love to see an abstract way of defining like "All instances of EntityRepository will return an entity of that type defined in getEntity() when loading an entity"
I doubt there's a blanket way of doing this. Even using PHPStorm meta you have to be explicit for each case. Perhaps the way of doing this is by doing something like adding a repository facade e.g.
class RepositoryFacade {
public static function __callStatic($method, $args) {
if ($args[0] == People::class) {
array_shift($args);
return new PeopleRepository()->{$method}(...$args);
}
}
}
Then you might be able to typehint this using:
override(RepositoryFacade::loadById(0), type(0));
Of course the facade is not really the best pattern to be using in general so I can see how this might not be ideal.

A new entity was found through the relationship that was not configured to cascade persist operations for entit

New to doctrine, trying to insert object that has ManyToOne rel into database, but getting errors. Writing relevant code below:
Controller:
$oPost = $this->getDoctrine()->getRepository('AppBundle:RedditPost')->findOneById(5);
$oComment = new \AppBundle\Entity\Comments();
$oComment->setComment('4');
$oComment->setCreated('2016-11-12');
$oPost->addComment($oComment);
$oEm = $this->getDoctrine()->getManager();
$oEm->persist($oPost);
$oEm->flush(); // -- getting error here
Entity\RedditPost:
public function __construct()
{
$this->comments = new ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Comments", mappedBy="post")
*/
protected $comments;
public function addComment(Comments $comments)
{
if (!$this->comments->contains($comments)) {
$comments->setPost($this);
$this->comments->add($comments);
}
return $this;
}
Entity\Comments:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\RedditPost", inversedBy="comments")
* #ORM\JoinColumn(name="post_id", referencedColumnName="id")
*/
protected $post;
public function setPost(RedditPost $post)
{
$this->post = $post;
return $this;
}
Full error:
A new entity was found through the relationship 'AppBundle\Entity\RedditPost#comments' that was not configured to cascade persist operations for entity: AppBundle\Entity\Comments#000000005d78d3ae00000000b58ca229. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'AppBundle\Entity\Comments#__toString()' to get a clue.
What i tried and notes:
From the error, i tried adding the ,cascade={"persist"} value to Comments entity, but error didnt change at all
Tried making 2 persist calls in controller $oEm->persist($oPost); $oEm->persist($oComment); - but i get ERR_CONNECTION_RESET error and cant debug from there.
I checked my database and all the relations are there, everything seems fine.
I can insert into RedditPost entity without problems.
Tried clearing cache.
Made new attempt at this, still same results:
class Owner {
public function __construct()
{
$this->children = new ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Child", mappedBy="owner")
*/
protected $children;
public function addChild(Child $child)
{
if (!$this->children->contains($child)) {
$child->setOwner($this);
$this->children->add($child);
}
return $this;
}
}
class Child {
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Owner", inversedBy="children", cascade={"persist"})
* #ORM\JoinColumn(name="owner_id", referencedColumnName="id")
*/
protected $owner;
/**
* #param mixed $owner
*/
public function setOwner(Owner $owner)
{
$this->owner = $owner;
return $this;
}
}
class DefaultController extends Controller
{
public function indexAction()
{
$owner = new Owner();
$child = new Child();
$child->setName('test');
$owner->addChild($child);
$em = $this->getDoctrine()->getManager();
$em->persist($owner);
$em->flush();
}
}
Current solution is to remove #ORM\OneToMany completely, that way i can insert to database without problems.
Yes, you shouldn't have #ORM\OneToMany from Post to Comment. In this scenario, you should use Unidirectional relationship instead of Bidirectional, and handle relationship from Comment entity (where foreign key is available). You might want to check this blog for reference.
So, what you loose when you remove #ORM\OneToMany relationship. Literally, nothing. When you want to retrieve list of Comments for a single Post write a repository and use it.
Found the problem..
I had a timestamp field in my table, which i tried to set like this:
$oComment->setCreated('2016-01-01');
This is the reason i could not use $em->persist($oComment) (it returned the ERR_CONNECTION_RESET error).
Solution was $oComment->setCreated(new \DateTime('2016-01-01'));.
This ERR_CONNECTION_RESET error displays nothing and it makes debuging a lot harder.

Dynamically generate translatable columns using metadata

I would like to make a Translatable behavior for my entities using metadata.
I have a class Article
class Article implements TranslatableInterface {
/**
* #HeidanTranslatable
*/
private $title;
/**
* #HeidanLocale
*/
private $locale;
public function getTitle() {
return $this->title;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getLocale() {
return $this->locale;
}
public function setLocale($locale) {
$this->locale = $locale;
return $this;
}
I would like to have kind of Gedmo Doctrine Extension behavior which will create in database columns depending on property and allowed locales.
For example, with the entity article, I would like that two columns are created : title_fr, title_en.
I'd like this stuff is bridged to Doctrine behavior and I made a loadClassMetadataListener
class LoadClassMetadataListener {
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$metadata = $eventArgs->getClassMetadata();
$metadata
->mapField(array('fieldName' => 'title_fr', 'type' => 'text'))
;
}
When I run a doctrine:schema:update --force I have the following error :
[ReflectionException]
Property Heidan\CoreBundle\Entity\Article::$title_fr does not exist
So I guess they said that the property title_fr does not exist, and that's right.
I do not want to set manually properties (private $title_fr, private $title_en, private $content_fr, private $content_en) for all my entities.
Is there any way to achieve this behavior so far ?
Thanks a lot for your help.

Entities and Mappings in Doctrine

I'm trying to learn Doctrine2, and am having some trouble wrapping my brain around Entities and Mappings, and how to pull data from the db.
If I understand correctly, an Entity would be created for data in my db, like a User or a Product.
That's where I'm getting hung up though. I have a simple database with tables like User and such. Do I need to create an Entity called "User"? And does that entity pull data from the User table, and then I get data from the User entity?
Can someone provide me a code example of how I would do this?
Entities are regular PHP classes... First u have to create ur table, then u create your entity. Properties of class should be private with setters and getters and usually with same names as field in table. When U want to put new record in DB, u must make instance of class u want and set values then persist and flush the entity manager. Example code:
<?php
namespace Entity;
/**
* #Entity
* #Table(name="users")
*/
class User {
/** #Id
* #Column(type="integer")
* */
private $id;
/** #Column(type="string") */
private $username;
/** #Column(type="string") */
private $password;
/** #Column(type="boolean") */
private $active;
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
public function getUsername() {
return $this->username;
}
public function setUsername($username) {
$this->username = $username;
}
public function getPassword() {
return $this->password;
}
public function setPassword($password) {
$this->password = $password;
}
public function getActive() {
return $this->active;
}
public function setActive($active) {
$this->active = $active;
}
}
and when u want to put a new record:
$user = new Entity\User();
$user->setName('users name');
$user->setPassword('password');
$entityManager->persist($user); // put that entity in queue;
$entityManager->flush(); // execute all pending entities
If u want to get existing record
$found = $entityManager->find('className', $id); // search by id
or $entityManager->getRepository('className')->findOneBy(array('field', 'value'));

Categories