Integrity constraint violation in Magento custom module - sql

I have a similar probelm to Integrity constraint violation creating Product in Magento (unanswered) but I am creating a custom Observer that hooks into the catalog_product_save_after event - based on this tutorial: http://fishpig.co.uk/blog/custom-tabs-magento-product-admin.html
However whenever a new product is saved I get this error:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '22-1' for key 'UNQ_CATALOGINVENTORY_STOCK_ITEM_PRODUCT_ID_STOCK_ID'
The config.xml looks like this:
<adminhtml>
<events>
<catalog_product_save_after>
<observers>
<a1web_save_product_data>
<type>singleton</type>
<class>metricimperial/observer</class>
<method>saveProductData</method>
</a1web_save_product_data>
</observers>
</catalog_product_save_after>
</events>
</adminhtml>
The outline of the class is like this:
<?php
class A1web_MetricImperialConverter_Model_Observer
{
/**
* Flag to stop observer executing more than once
*
* #var static bool
*/
static protected $_singletonFlag = false;
* #param Varien_Event_Observer $observer
*/
public function saveProductData(Varien_Event_Observer $observer)
{
if (!self::$_singletonFlag) {
self::$_singletonFlag = true;
$product = $observer->getEvent()->getProduct();
//Custom updates made to product object here
$product->save();
}
catch (Exception $e) {
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
}
}
}
/**
* Retrieve the product model
*
* #return Mage_Catalog_Model_Product $product
*/
public function getProduct()
{
return Mage::registry('product');
}
/**
* Shortcut to getRequest
*
*/
protected function _getRequest()
{
return Mage::app()->getRequest();
}
}
The product is saved correctly with the custom product data I'm adding - and once the product is saved the error does not occur on subsequent saves of the same product. It is just when the product is first created the error occurs.
Thanks in advance

Instead of using $product->save() try using the resource model, a la $product->getResource()->save($product).
The reason being $product->save() will re-trigger all save events, hence running whatever is saving the cataloginventory_stock and throwing the error.

In this case, I'd recommend not using the catalog_product_save_after event. Instead, try using catalog_product_prepare_save which is fired after the POST data is applied to the product, but before ->save() is called. That way you don't have to mess with with saving or that ugly $_singletonFlag!
Also, with catalog_product_prepare_save you get the HTTP Request object inside the Observer Event. No need for Mage::app()->getRequest(). Woot!

Related

Yii2 Event not run after login

I created a Behavior which contains function. This function should be afterLogon of User (yii/web/User::EVENT_AFTER_LOGIN).
But this function never will be triggered unfortunatelly.
I have a Behaviour class for the user model:
class UserBehavior extends Behavior
{
/**
* #inheritdoc
* #param \yii\base\Component $owner
*/
public function attach($owner)
{
parent::attach($owner);
$owner->on(\yii\web\User::EVENT_AFTER_LOGIN, [$this, 'updateLoginInformation']);
}
/**
* Update login information data:
* - login ip address
* - login time
*/
public function updateLoginInformation()
{
/** #var \common\models\User $owner */
$owner = $this->owner;
$owner->logged_in_ip = Yii::$app->request->getUserIP();
$owner->logged_in_at = time();
$owner->save();
}
}
I declared the events and the attach too.
But this events never be run after login...
I attached this behavior to the user model:
/**
* #inheritdoc
*/
public function behaviors()
{
return [
TimestampBehavior::className(),
UserBehavior::className()
];
}
If I know well the the EVENT_AFTER_LOGIN will be triggered automatically by the Yii framework, this is the reason why I do not trigger it again.
And I do not where is the problem, because the updageLoginInformatin never called.
I usually use any logic I want in a model inside the proper action that calls it (IE: actionLogin). But I like your approach.
I just made a test here and the correct way to call the event is something like this:
$user = \Yii::$app->user;
$user->on($user::EVENT_AFTER_LOGIN, [$this, 'updateLoginInformation']);
I didn't create a behavior class, I just added this lines in my init(), but the logic is probably the same as yours.

Modelling aggregate root or domain service?

I want to model a wishlisting feature for my domain.
My invariants are:
You can't add product that is already in your wishlist
You can't add product that you own.
The second invariant made me wonder - should I model this feature as reconstituted Aggregate (outside of ORM because of $ownedProductIds that be fetched from UserProductRepository):
final class User extends EventSourcedAggregateRoot
{
// ...
/**
* #param UserId $userId
* #param ObjectCollection $ownedProductIds
* #param ObjectCollection $wishlistedProductIds
* #return $this
*/
public static function reconstituteFrom(
UserId $userId,
ObjectCollection $ownedProductIds,
ObjectCollection $wishlistedProductIds
)
{
$user = new User();
$user->userId = $userId;
$user->ownedProductIds = $ownedProductIds;
$user->wishlistedProductIds = $wishlistedProductIds;
return $user;
}
/**
* #param Product $product
* #throws ProductAlreadyPurchased Thrown when trying to add already bought product
* #throws ProductAlreadyWishlisted Thrown when trying to add already wishlisted product
*/
public function addProductToWishlist(Product $product)
{
$productId = $product->getId();
if ($this->ownedProductIds->contains($productId)) {
throw new ProductAlreadyPurchased($this->userId, $productId);
}
if ($this->wishlistedProductIds->contains($productId)) {
throw new ProductAlreadyWishlisted($this->userId, $productId);
}
$this->apply(new ProductWishlisted($this->userId, $product));
}
// ...
}
or rather create a stateless domain service:
final class Wishlist
{
public function addProductToWishlist(Product $product, UserId $userId)
{
$ownedProductids = $this->userProductRepository->findProductsOfUser($userId);
$wishlistedProductsIds = $this->userWishlistProductIdRepository->findProductsOfUser($userId);
// business rules as in User class
}
}
As User has all the information needed to enforce the invariants, I would leave it there. I typically only create Domain Services when some operation doesn't seem to belong in one entity (TransferMoney() method not fitting in Account class is the poster child for this).
Note though that your model is currently really simplistic. As it is, it may make sense to name the aggregate User, but in a real situation chances are you will make breakthroughs that completely change it.

Tx_Extbase_Domain_Repository_FrontendUserRepository->findAll() not working in typo3 4.5.30?

I am trying to run a simple query off of the Tx_Extbase_Domain_Repository_FrontendUserRepository. I cannot get anything to work except findByUid(), not even findAll().
In my controller I have this code which seems to work:
/**
* #var Tx_Extbase_Domain_Repository_FrontendUserRepository
*/
protected $userRepository;
/**
* Inject the user repository
* #param Tx_Extbase_Domain_Repository_FrontendUserRepository $userRepository
* #return void */
public function injectFrontendUserRepository(Tx_Extbase_Domain_Repository_FrontendUserRepository $userRepository) {
$this->userRepository = $userRepository;
}
/**
* action create
*
* #param Tx_BpsCoupons_Domain_Model_Coupon $newCoupon
* #return void
*/
public function createAction(Tx_BpsCoupons_Domain_Model_Coupon $newCoupon) {
...... some code .....
$user = $this->userRepository->findByUid(($GLOBALS['TSFE']->fe_user->user[uid]));
$newCoupon->setCreator($user);
...... some code .....
}
but in another function I want to look up a user not by uid but by a fe_users column called vipnumber (an int column) so I tried
/**
* check to see if there is already a user with this vip number in the database
* #param string $vip
* #return bool
*/
public function isVipValid($vip) {
echo "<br/>" . __FUNCTION__ . __LINE__ . "<br/>";
echo "<br/>".$vip."<br/>";
//$ret = $this->userRepository->findByUid(15); //this works!! but
$query = $this->userRepository->createQuery();
$query->matching($query->equals('vip',$vip) );
$ret = $query->execute(); //no luck
.................
and neither does this
$ret = $this->userRepository->findAll();
How can one work but not the others? In my setup I already put
config.tx_extbase.persistence.classes.Tx_Extbase_Domain_Model_FrontendUser.mapping.recordType >
which seems to be necessary for the fiondByUid to work, is i t preventing the other from working?
I am using typo3 v 4.5.30 with extbase 1.3
Thanks
If $this->userRepository->findByUid(15); works, there is no reason why $this->userRepository->findAll(); should not. However $this->userRepository->findAll(); returns not a single Object but a collection of all objects, so you have to iterate over them.
If you add a column to the fe_users, you have to add it to TCA and to your extbase model (you need a getter and a setter), too! After that you can call findByProperty($property) in your repository. In your case that would be
$user = $this->userRepository->findByVipnumber($vip);
This will return all UserObjects that have $vip set as their Vipnumber. If you just want to check if that $vip is already in use, you can call
$user = $this->userRepository->countByVipnumber($vip);
instead. Which obviously returns the number of Users that have this $vip;
You never use $query = $this->createQuery(); outside your Repository.
To add the property to the fronenduser Model you create your own model Classes/Domain/Model/FronendUser.php:
class Tx_MyExt_Domain_Model_FrontendUser extends Tx_Extbase_Domain_Model_FrontendUser {
/**
* #var string/integer
*/
protected $vipnumber;
}
Add a getter and a setter. Now you create your own FrontendUserRepository and extend the extbase one like you did with the model. You use this repository in your Controller. Now you're almost there: Tell Extbase via typoscript, that your model is using the fe_users table and everything should work:
config.tx_extbase {
persistence{
Tx_MyExt_Domain_Model_FrontendUser{
mapping {
tableName = fe_users
}
}
}
}
To disable storagePids in your repository in general, you can use this code inside your repository:
/**
* sets query settings repository-wide
*
* #return void
*/
public function initializeObject() {
$querySettings = $this->objectManager->create('Tx_Extbase_Persistence_Typo3QuerySettings');
$querySettings->setRespectStoragePage(FALSE);
$this->setDefaultQuerySettings($querySettings);
}
After this, your Querys will work for all PIDs.
I didn't have the opportunity to work with frontend users yet, so I don't know if the following applies in this case:
In a custom table I stumbled uppon the fact, that extbase repositories automatically have a look at the pids stored in each entry and check it against a set storage pid (possibly also the current pid if not set). Searching for a uid usually means you have a specific dataset in mind so automatic checks for other values could logically be ignored which would support your experiences. I'd try to set the storage pid for your extension to the place the frontend users are stored in ts-setup:
plugin.[replace_with_extkey].persistence.storagePid = [replace_with_pid]

Pros & cons bean vs SSJS?

I was trying to build a bean that always retrieves the same document ( a counter document), gets the current value, increment it and save the document with the new value. Finally it should return the value to the calling method and that would get me a new sequential number in my Xpage.
Since the Domino objects cannot be serialized or singleton'ed what's the benefit creating a bean doing this, over creating a SSJS function doing the exact same thing?
My bean must have calls to session, database, view and document, which then will be called every time.
The same within the SSJS-function except for session and database.
Bean:
public double getTransNo() {
try {
Session session = ExtLibUtil.getCurrentSession();
Database db = session.getCurrentDatabase();
View view = db.getView("vCount");
view.refresh();
doc = view.getFirstDocument();
transNo = doc.getItemValueDouble("count");
doc.replaceItemValue("count", ++transNo);
doc.save();
doc.recycle();
view.recycle();
} catch (NotesException e) {
e.printStackTrace();
}
return transNo;
}
SSJS:
function getTransNo() {
var view:NotesView = database.getView("vCount");
var doc:NotesDocument = view.getFirstDocument();
var transNo = doc.getItemValueDouble("count");
doc.replaceItemValue("count", ++transNo);
doc.save();
doc.recycle();
view.recycle();
return transNo;
}
Thank you
Both pieces of code are not good (sorry to be blunt).
If you have one document in your view, you don't need a view refresh which might be queued behind a refresh on another view and be very slow. Presumably you are talking about a single sever solution (since replication of the counter document would for sure lead to conflicts).
What you do in XPages is to create a Java class and declare it as application bean:
public class SequenceGenerator {
// Error handling is missing in this class
private double sequence = 0;
private String docID;
public SequenceGenerator() {
// Here you load from the document
Session session = ExtLibUtil.getCurrentSession();
Database db = session.getCurrentDatabase();
View view = db.getView("vCount");
doc = view.getFirstDocument();
this.sequence = doc.getItemValueDouble("count");
this.docID = doc.getUniversalId();
Utils.shred(doc, view); //Shred currenDatabase isn't a good idea
}
public synchronized double getNextSequence() {
return this.updateSequence();
}
private double updateSequence() {
this.sequence++;
// If speed if of essence I would spin out a new thread here
Session session = ExtLibUtil.getCurrentSession();
Database db = session.getCurrentDatabase();
doc = db.getDocumentByUnid(this.docID);
doc.ReplaceItemValue("count", this.sequence);
doc.save(true,true);
Utils.shred(doc);
// End of the candidate for a thread
return this.sequence;
}
}
The problem for the SSJS code: what happens if 2 users hit that together? At least you need to use synchronized there too. Using a bean makes it accessible in EL too (you need to watch out not to call it too often). Also in Java you can defer the writing back to a different thread - or not write it back at all and in your class initialization code read the view with the actual documents and pick the value from there.
Update: Utils is a class with static methods:
/**
* Get rid of all Notes objects
*
* #param morituri = the one designated to die, read your Caesar!
*/
public static void shred(Base... morituri) {
for (Base obsoleteObject : morituri) {
if (obsoleteObject != null) {
try {
obsoleteObject.recycle();
} catch (NotesException e) {
// We don't care we want go get
// rid of it anyway
} finally {
obsoleteObject = null;
}
}
}
}

Doctrine 2.1 Persist entity in preUpdate lifeCycleCallback

I'm struggling with the following, in a entity class I have a preUpdate lifeCycleCallback which has to persist a new entity before it flushes the changes for a auditTrail.
In preRemove and prePersist this works perfectly but in preUpdate nothing happends. If I call flush myself it goes in a recursive loop.
According to the Google groups for doctrine-user putting it in onFlush should be a option but in that event I can't access the old values of the entity to save this old values in a new other entity for the audittrail.
Some small example what i'm trying to archive:
<?php
/**
* #Entity
* #HasLifeCycleCallbacks
*/
class someEntity {
... annotations ...
/**
* #PreUpdate
*/
public function addAuditTrail() {
$em = \Zend_Registry::get('doctrine')->getEntityManager();
$entity = new AuditTrail();
$entity->action = 'update';
$entity->someField = $this->someField;
$em->persist($entity); //this just doesn't do anything :-(
}
}
?>
It's not real code, just something to illustrate you what I want. I also tried something like this:
$em->getUnitOfWork()->computeChangeSet($em->getClassMetaData(get_class($entity)), $entity);
Which should work according to this topic: http://groups.google.com/group/doctrine-user/browse_thread/thread/bd9195f04857dcd4
If I call the flush again but that causes Apache to crash because of some infinite loop.
Anyone who got ideas for me? Thanks!
You should never use the entitymanager inside your entities. If you would like to add audit trails, you should map the "SomeEntity" entity to the "AuditTrail" entity and do something like
/**
* #PreUpdate
*/
public function addAuditTrail() {
$entity = new AuditTrail();
$entity->action = 'update';
$entity->someField = $this->someField;
$this->autitTrail->add($entity);
}
If you set the cascade option on the mapping, it will get persisted when you persist "SomeEntity".
I had the same problem in the preUpdate method of an EventListener. I solved this by storing the new entity in a property and moving the new persist() and flush() calls to the postUpdate method.
class someEntity {
... annotations ...
protected $store;
/**
* #PreUpdate
*/
public function addAuditTrail() {
//$em = \Zend_Registry::get('doctrine')->getEntityManager();
$entity = new AuditTrail();
$entity->action = 'update';
$entity->someField = $this->someField;
// replaces $em->persist($entity);
$this->store = $entity;
}
/**
* #PostUpdate
*/
public function saveAuditTrail() {
$em = \Zend_Registry::get('doctrine')->getEntityManager();
$em->persist($this->store);
$em->flush();
}
}
entitymanager->persist() will not work inside the preUpdate method.
Instead of that, you can save the AuditTrail data to session and after the flush of the 'SomeEntity', take the data from session and perform entitymanager->persist(...) and entitymanager->flush()