Showing posts with label doctrine2. Show all posts
Showing posts with label doctrine2. Show all posts

Thursday, April 26, 2018

Doctrine - map entity for ORM and ODM

Leave a Comment

I'm working on synchronization entities from one DB to another. I have entities mapped for ORM and ODM, like:

/*  * @ODM\Document(  *     repositoryClass="App\Lib\Repositories\ProductRepository",  *     collection="products"  * )  * @ODM\InheritanceType("COLLECTION_PER_CLASS")  *  * @ORM\Entity(  *     repositoryClass="App\Lib\Repositories\Legacy\LegacyProductRepository"  * )  * @ORM\Table(name="product")  *  * @ORM\HasLifecycleCallbacks()  * @ODM\HasLifecycleCallbacks()  */ class Product extends Article 

It works nice, but I would like to load entity from document manager from mongo db and save it to ORM:

$product = $this->documentManager->find(Product::class, $id); $this->entityManager->merge($product); $this->entityManager->flush(); 

But I have an issue with relations. How do I persist related entity (such as ProductAction) with merging a product?

1 Answers

Answers 1

If I understand correctly, you want to merge "ProductAction" related entities to the ORM when a Product entity is merged to it.

You can use , cascade={"merge"} on the relation., eg.

/**  * @ORM\OneToMany(targetEntity="App\Entity\ProductAction", mappedBy="product", cascade={"persist", "merge"})  */ private $productActions; 

Understanding cascade operations

Merging entities

Read More

Wednesday, February 7, 2018

Safely decrease User balance column. Should I use optimistic locking?

Leave a Comment

I have a simple Silex web app with MySQL/Doctrine ORM. Each User has balance (it's a simple app, so just column is fine) and I need to decrease it after some action (checking that it is > 0 of course).

As I understand I can use optimistic locking to avoid conflicts/vulnerabilities. I have read the docs http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html but I can't find any complete example about using it.

Where do I get the "expected version"? Do I need to pass it as input (hidden form field)? Or there are better ways? The docs say something about session but I don't get how I could store it there (update session on each request?).

Also if I pass it as input, then as I understand there is no way to repeat the query automatically after catching OptimisticLockException without notifying user about that? (for example if user opened two tabs and submitted the request in them one by one)

My goal is just to prevent potential issues when user sends several requests at the same time and balance gets decreased only once etc. So it would be good to be able to repeat it automatically on lock error without involving the user. Because if I pass it via form then getting this error because of multiple tabs is very likely. So it seems kind of complicated, maybe there is something else instead of optimistic locking?

3 Answers

Answers 1

Create a column named "version" in the "user" table and make it a "timestamp" column ( with "on update CURRENT_TIMESTAMP" attribute). So, "User" ORM class will look like below :

class User {     // ...     /** @Version @Column(type="timestamp") */     private $version;     // ... } 

Now, read the current record with its "version".

$theEntityId = YOUR ENTITY ID; $entity = $em->find('User', $theEntityId); $expectedVersion = entity->version; try {    // assert version     $em->lock($entity, LockMode::OPTIMISTIC, $expectedVersion);      // do the work      $em->flush(); }  catch(OptimisticLockException $e) {     echo "Sorry, but someone else has already changed this entity. Please apply the changes again!"; } 

Answers 2

You should only use locking for operations that can't be executed atomically. So if possible avoid querying the object, checking the amount and then updating it. If instead you do:

update user set balance = (balance + :amount)  where (balance + :amount) >= 0  and id = :user_id 

This you will check and update in one operation, updated rows count will be 1 if the check passed and the balance was updated and 0 otherwise.

Answers 3

If all of your actions are performed in single request i would suggest to use transaction:

$em->getConnection()->beginTransaction(); try {     // ... other actions on entities, eg creating transaction entity      $newBalance = $user->getBalance() - $value;       if (! $newBalance >= 0) {         throw new \Exception('Insufficient founds');     }      $user->setBalance($newBalance);      $em->persist($user);     $em->flush();     $em->getConnection()->commit(); } catch (\Exception $e) {     $em->getConnection()->rollBack();     throw $e; } 

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html#approach-2-explicitly

Read More

Friday, June 10, 2016

Create form for Uploadable Doctrine Extension

Leave a Comment

I'd like to use Uploadable to save some images (i.e. profile picture for users). I'm using many other Doctrine Extensions already (softdeletable, timestampable, blameable, etc.) so I thought it would be nice to use this one as well.

However, I don't know how to set up my Forms. The StofDoctrineExtensionsBundle documentation gives this example:

$document = new Document(); $form = $this->createFormBuilder($document)     ->add('name')     ->add('myFile')     ->getForm() ;  //(...)  $uploadableManager->markEntityToUpload($document, $document->getMyFile()); 

In this example, is name the name of the Document or the name of the file?

Atlantic18/DoctrineExtensions's documentation adds a path, name, mimeType and size to an entity, to there is no myFile attribute.

Can anybody explain how to set up a Form for a Uploadable Doctrine entity? I couldn't find any documentation or good example that helped me further.

1 Answers

Answers 1

Entity

Like you've discovered, the documentation of DoctrineExtensions itself sheds some light on how to configure the Uploadable extension to use your entity.

Mainly by adding the @Gedmo\Uploadable annotation to your entity and @Gedmo\UploadableFilePath to the property that will contain the file path ($filePath for example).

Form

With the ->add() method of the form-builder you add fields to the form. The first parameter specifies the property-name of the corresponding entity. So ->add('myFile') would add a field for the property $myFile.

What you need to do is add a (file) field to the form for the property that will contain the file path ($filePath for example), and mark that property:

$form = $this->createFormBuilder($entity)     ->add('filePath');  $uploadableManager->markEntityToUpload($entity, $entity->getFilePath()); 

In other words: myFile in your example should be replaced with filePath in my example, and whatever the actual property is in your real code.

Read More

Thursday, May 5, 2016

Using Doctrine and Symfony to create Polymorphic like associations

Leave a Comment

I'm attempting to have an Fileable trait that will give provide an Entity with methods to CRUD Files based on the File Entity mentioned below.

After reading the documentation on Doctrine and searching the internet, the best I could find is Inheritance Mapping but these all require the subclass to extend the superclass which is not ideal as the current Entities already extend other classes. I could have FileFoo entity and a FileBar entity but this gets too messy and requires an extra join (super -> sub -> entity).

Alternatively, I could have a File Entity which has many columns for Entities (so foo_id for the Foo object, bar_id for the bar object and so on) but this gets messy and would require a new column for every entity that I'd want to add the Fileable trait too.

So to the questions: Am I thinking about how I want to hold data incorrectly? Is there some features/functions in Doctrine/Symfony that I've missed? Do you think I feature like this would be added if I were to fork Doctrine to add this feature, also where should I look?

<?php /**  * File  *  * @ORM\Table()  * @ORM\Entity()  * @ORM\HasLifecycleCallbacks()  */ class File {     /**      * @var integer      *      * @ORM\Column(type="integer")      * @ORM\Id()      * @ORM\GeneratedValue()      */     protected $id;     /**      * @var string      *      * @ORM\Column(type="string")      */     protected $entityName;     /**      * @var string      *      * @ORM\Column(type="string")      */     protected $entityId; ... 

2 Answers

Answers 1

I accomplished a similar thing using Inheritance defined in traits, which alongside interfaces, basically gave me what a multiple extend would give.

Answers 2

Take a look at embeddables or you could use traits.

Read More

Wednesday, April 13, 2016

Zend framework 2 / Doctrine 2 / Bulk operations and events triggering

Leave a Comment

For a huge project, with a lot of entities, I wrote a save() common method.

This method is stored in an abstract service and is used in all the project to save entities state.

AbstractService::save() looks like this :

public function save($entity) {     $transactionStarted = $this->beginTransaction();      try     {         $action = $entity->getId() ? self::UPDATE : self::CREATION;          $this->getEventManager()->trigger('save.pre', $entity, ['action' => $action]);          $this->getEntityManager()->persist($entity);         $this->getEntityManager()->flush();          $this->getEventManager()->trigger('save.post', $entity, ['action' => $action]);          if ($transactionStarted)         {             $this->commitTransaction();         }     } catch (\Exception $e)     {         if ($transactionStarted)         {             $this->rollbackTransaction();         }          throw new Exception('Unable to save entity', $e);     }      return true; }  public function beginTransaction() {     if (!$this->getEntityManager()->getConnection()->isTransactionActive())     {         $this->getEntityManager()->getConnection()->beginTransaction();          return true;     }      return false; }  public function commitTransaction() {     $this->getEntityManager()->getConnection()->commit();      return $this; }  public function rollbackTransaction() {     $this->getEntityManager()->getConnection()->rollBack();      return $this; } 

In my case, when a member is inserted (new Member entity) when calling the Member service (extended AbstractService), an email is sent (e.g) through the save.post event. Or another action related to another service calling save method too can be proceed.

Example of the "child" MemberService::save() method

MemberService  public function save(Member $member) {     // some stuff, e.g set a property     $member->setFirstName('John');      return parent::save($member); } 

Example of triggered event

$sharedEventManager->attach(MemberService::class, 'save.post', [$this, 'onMembersCreation']);  public function onMembersCreation(EventInterface $event) {     // send an email      // anything else ... update another entity ... (call AnotherService::save() too)  } 

That's great for a simple saving process.

But now, I want to massively import a lot of members, with creations, updates, ... And to achieve that, I read the Doctrine doc related to bulk imports. Doc here

But how to update my code properly to handle "bulk saving" and "single saving" ? And keep transactions security and events ?

1 Answers

Answers 1

Basically I suggest you implement the Doctrine\Common\Collections\Collection interface, maybe extending ArrayCollection, and create a method save that will do what the doc told you to.

<?php  class MyDirtyCollection extends \Doctrine\Common\Collections\ArrayCollection {      public function __construct(AbstractService $abstractService)     {         $this->service = $abstractService;     }      public function save()     {         foreach ($this as $entity) {             $this->service->save($entity);         }     } }  class MyCollection extends \Doctrine\Common\Collections\ArrayCollection {      public $bulkSize = 500;      protected $eventManager;     protected $entityManager;      public function __construct(EntityManager $entityManager, EventManager $eventManager)     {         $this->entityManager = $entityManager;         $this->eventManager = $eventManager;     }      public function getEventManager()     {         return $this->eventManager;     }      public function getEntityManager()     {         return $this->entityManager;     }      public function setBulkSize(int $bulkSize)     {         $this->bulkSize = $bulkSize;     }      public function save()     {         $transactionStarted = $this->getEntityManager()->getConnection()->beginTransaction();          try {             foreach ($this as $entity) {                 $action = $entity->getId() ? self::UPDATE : self::CREATION;                 $this->getEventManager()->trigger('save.pre', $entity, ['action' => $action]);             }              $i = 0;             foreach ($this as $entity) {                 $i++;                  $this->getEntityManager()->persist($entity);                  if (($i % $this->bulkSize) === 0) {                     $this->getEntityManager()->flush();                     $this->getEntityManager()->clear();                 }             }              $this->getEntityManager()->flush();             $this->getEntityManager()->clear();              foreach ($this as $entity) {                 $action = $entity->getId() ? self::UPDATE : self::CREATION;                 $this->getEventManager()->trigger('save.post', $entity, ['action' => $action]);             }              if ($transactionStarted) {                 $this->getEntityManager()->getConnection()->commitTransaction();             }          } catch (Exception $e) {             $this->getEntityManager()->rollbackTransaction();         }     } } 

Something like that ;) When you fetch your data you hydrate your collection, then you deal with your entity and finally call $collection->save();

EDIT : Add insert class and use case below :

The performance here will be low, but still better than commit by commit. Yet you should think about using Doctrine DBAL instead of the ORM if you are looking for hgih performance. Here I share with you my DBAL class for bulk Insert :

<?php  namespace JTH\Doctrine\DBAL;  use Doctrine\DBAL\Query\QueryBuilder; use Exception; use InvalidArgumentException; use Traversable; use UnderflowException;  class Insert extends QueryBuilder {     const CALLBACK_FAILURE_SKIP = 0;     const CALLBACK_FAILURE_BREAK = 1;      protected $callbackFailureStrategy = self::CALLBACK_FAILURE_BREAK;      public static $defaultBulkSize = 500;      public $ignore = false;     public $onDuplicate = null;      public function values(array $values)     {         $this->resetQueryPart('values');         $this->addValues($values);     }      public function addValues(array $values)     {         $this->add('values', $values, true);     }      public function setCallbackFailureStrategy($strategy)     {         if ($strategy == static::CALLBACK_FAILURE_BREAK) {             $this->callbackFailureStrategy = static::CALLBACK_FAILURE_BREAK;         } elseif ($strategy == static::CALLBACK_FAILURE_SKIP) {             $this->callbackFailureStrategy = static::CALLBACK_FAILURE_SKIP;         } else {             $class = self::class;             throw new InvalidArgumentException(                 "Invalid failure behaviour. See $class::CALLBACK_FAILURE_SKIP and $class::CALLBACK_FAILURE_BREAK"             );         }     }      public function getCallbackFailureStrategy()     {         return $this->callbackFailureStrategy;     }      public function execute()     {         return $this->getConnection()->executeUpdate(             $this->getSQLForInsert(),             $this->getParameters(),             $this->getParameterTypes()         );     }      /**      * Converts this instance into an INSERT string in SQL.      * @return string      * @throws \Exception      */     private function getSQLForInsert()     {         $count = sizeof($this->getQueryPart('values'));          if ($count == 0) {             throw new UnderflowException("No values ready for INSERT");         }          $values = current($this->getQueryPart('values'));         $ignore = $this->ignore ? 'IGNORE' : '' ;         $sql = "INSERT $ignore INTO " . $this->getQueryPart('from')['table'] .             ' (' . implode(', ', array_keys($values)) . ')' . ' VALUES ';          foreach ($this->getQueryPart('values') as $values) {             $sql .= '(' ;              foreach ($values as $value) {                 if (is_array($value)) {                     if ($value['raw']) {                         $sql .= $value['value'] . ',';                     } else {                         $sql .= $this->expr()->literal($value['value'], $value['type']) . ',';                     }                 } else {                     $sql .= $this->expr()->literal($value) . ',';                 }             }              $sql = substr($sql, 0, -1);             $sql .= '),';         }          $sql = substr($sql, 0, -1);          if (!is_null($this->onDuplicate)) {             $sql .= ' ON DUPLICATE KEY UPDATE ' . $this->onDuplicate . ' ';         }          return $sql;     }      /**      * @param $loopable array | Traversable An array or object to loop over      * @param $callable Callable A callable that will be called before actually insert the row.      * two parameters will be passed :      * - the key of the current row      * - the row values (Array)      * An array of rows to insert must be returned      * @param $bulkSize int How many rows will be inserted at once      * @param bool $transactionnal      * @throws \Doctrine\DBAL\ConnectionException      * @throws \Exception      */     public function bulk($loopable, callable $callable, $bulkSize = null, $transactionnal = true)     {         if (!is_array($loopable) and !($loopable instanceof Traversable)) {             throw new InvalidArgumentException("\$loppable must be either an array or a traversable object");         }          $bulkSize = $bulkSize ?? static::$defaultBulkSize;          $this->getConnection()->getConfiguration()->setSQLLogger(null); // Avoid MonoLog memory overload          if ($transactionnal) {             $this->getConnection()->beginTransaction();         }          $this->resetQueryPart('values');          foreach ($loopable as $key => $values) {             try {                 $callbackedValues = $callable($key, $values);                  if (sizeof($callbackedValues) > 0) {                     foreach ($callbackedValues as $callbackedValuesRow) {                         $this->addValues($callbackedValuesRow);                     }                 }             } catch (Exception $e) {                 /*                  * If a callback exception must break the transaction, then throw the exception to the call stack                  * Else, skip the row insertion                  */                 if ($this->callbackFailureStrategy == static::CALLBACK_FAILURE_BREAK) {                     throw $e;                 } else {                     continue;                 }             }              $count = count($this->getQueryPart('values'));              if ($count >= $bulkSize) {                 $this->execute();                 $this->resetQueryPart('values');             }         }          $count = count($this->getQueryPart('values'));          if ($count > 0) {             $this->execute();         }          $this->resetQueryPart('values');          if ($transactionnal) {             $this->getConnection()->commit();         }     }      /**      * @return boolean      */     public function isIgnore()     {         return $this->ignore;     }      /**      * @param boolean $ignore      */     public function setIgnore(bool $ignore)     {         $this->ignore = $ignore;     }      /**      * @return null|string      */     public function getOnDuplicate() : string     {         return $this->onDuplicate;     }      /**      * @param null $onDuplicate      */     public function setOnDuplicate($onDuplicate)     {         $this->onDuplicate = $onDuplicate;         $this->ignore = false;     }   } 

Use case :

    try {         $i = new Insert($this->getDoctrine()->getConnection('myDB'));         $i->insert('myTable');         $i->setOnDuplicate('col1 = VALUES(col1), updated_last = NOW()');         $i->setCallbackFailureStrategy(Insert::CALLBACK_FAILURE_BREAK);         $i->bulk($myArrayOfRows, function ($key, $row) {              // Some pre-insert processing              $rowset[] = $row;              return $rowset;          }, 500, true);          $this->addFlash('success', 'Yay !');      } catch (DBALException $e) {         $this->addFlash('error', 'Damn, error : ' . $e->getMessage());     } 
Read More

Monday, March 28, 2016

Doctrine 2 Symfony 2 Getting foreign key entities without mapping

Leave a Comment

so I am fairly new to Symfony and Doctrine. I would like to know if there's a way to ask doctrine what foreign keys are in place, but without having to map relationships in the model.

For example, say you have CoreBundle:Company which is ALWAYS going to be present, and then you have OptionalBundle:Client which will extend Company with a @OneToOne mapping relationship, adding a few more fields in itself. The thing is, that since OptionalBundle may not be present, I don't want explicit mapping from CoreBundle to OptionalBundle.

Now say a user comes along and attempts to delete Company(5). If the entity was fully mapped it would delete both with cascading, but since the bundle is not going to be aware of a mapped relationship it would end up deleting the Company only - I want to produce an error rather than cascading the deletion.

If this is possible quite easily, then I would also want to take it another step further and say, what entities (class and id) have foreign keys that I can show the data to the user, like

@CoreBundle:Company(5) ->     has @OptionalBundle:Client(3) linked, and     has @AnotherOptionalBundle:Supplier(12) linked 

My first instinct is to do a custom INFORMATION_SCHEMA lookup for the foreign keys but that will only give me table names...

PS I REALLY prefer not to have to use any third party vendors as I like to try and keep the dependencies down, even if it means reinventing the wheel

3 Answers

Answers 1

Have you considered defining the relationship as owned by the OptionalBundle side?

Answers 2

The only idea I come across is to pre-create class-mapping during Compiler Pass with some fallback type when secondary bundle is absent.

In compiler pass, check whether container has a secondary bundle loaded and use DoctrineOrmMappingsPass::createXmlMappingDriver with adjusted path. If found - map with secondary bundle's entity, if not - map it to null (for example).

Answers 3

Question 1

You could set the Client as the owner of the 1-to-1 relationship. However, depending on your use-case it might not be ideal, but if that works for you it would really be the simplest solution, as pointed out by ABM_Dan.

Barring that, the best option for you is probably to use Doctrine event subscribers and to hook on the preDelete event, where you would remove the associated Client, before the Company itself is removed - if cascading the deletion is really what you want.

By default both deletion will be in the same Doctrine transaction, meaning that if something goes wrong when deleting the Company, the Client deletion will be cancelled.

If you really want to trigger an error instead of this "manual cascading" of sorts, it is also possible in the preDelete method of the Doctrine subscriber.

The subscriber class can reside in your optional bundle even though it will act on an event associated to Company.

Doctrine event subscribers are separate from the regular Symfony event system. Newcomers often are not aware of its existence, but it can achieve a lot of interesting things.

Question 2

Still in your event subscribers, it is possible to hook on the postLoad event. This would allow you to request the database and load related entities directly into Company. You can create an event subscriber for Company in each bundle that requires it.

Although this is possible I really wonder if there might not be a better way. Using decorators might be a better solution. I found a Doctrine cookbook article about it.

Read More