How to add a custom field to the session table - session

I'm currently using Symfony 2.1.8 and the built-in PdoSessionHandler.
I want to add a user_id field in the session table to identify to which (logged-in) user the session belongs. The idea is that I can force the user to log back in destroying his session. In my case it will happen if the privileges of the user are updated.
I had a look to the build-in PdoSessionHandler that you can't extends because of those silly private variables.
So I've tried created a new one (copy/paste) and add my column user_id.
Now this column can be null if the user is not logged in (anonymous users).
So I want to write this user_id in the write method of the handler. The user is already stored in the $data so I was thinking that I could check if this user exists, grab its id and add it in the insert / update query.
The problem is that $data is encoded - I guess by session_encode() - so I'm not sure anymore this is the best place ever to handle my new field, but at the same time I can't see anywhere else I could do it as I need to update this MySQL query to insert the value of the new field.
So my question is: where is the best place to handle this additional field? And how to set this user_id value?
On another note, somehing really annoying is that Symfony is creating a new cookie each time I'm logging in or out. So the database ends up with lots of records for nothing (it's always the same user). Why Symfony is not using the same cookie value all the time?

You could extend PdoSessionHandler (solution for >=Symfony 2.1):
namespace Acme\DemoBundle\HttpFoundation\Session\Storage\Handler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
use Symfony\Component\Security\Core\SecurityContext;
class UserIdPdoSessionHandler extends PdoSessionHandler
{
/**
* #var \PDO PDO instance.
*/
private $pdo;
/**
* #var array Database options.
*/
private $dbOptions;
/**
* #var SecurityContext
*/
private $context;
public function __construct(\PDO $pdo, array $dbOptions = array(), SecurityContext $context)
{
$this->pdo = $pdo;
$this->dbOptions = array_merge(
array('db_user_id_col' => 'user_id'),
$dbOptions
);
$this->context = $context;
parent::__construct($pdo, $dbOptions);
}
public function read($id)
{
// get table/columns
$dbTable = $this->dbOptions['db_table'];
$dbDataCol = $this->dbOptions['db_data_col'];
$dbIdCol = $this->dbOptions['db_id_col'];
try {
$sql = "SELECT $dbDataCol FROM $dbTable WHERE $dbIdCol = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
$stmt->execute();
// it is recommended to use fetchAll so that PDO can close the DB cursor
// we anyway expect either no rows, or one row with one column. fetchColumn, seems to be buggy #4777
$sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM);
if (count($sessionRows) == 1) {
return base64_decode($sessionRows[0][0]);
}
// session does not exist, create it
$this->createNewSession($id);
return '';
} catch (\PDOException $e) {
throw new \RuntimeException(sprintf('PDOException was thrown when trying to read the session data: %s', $e->getMessage()), 0, $e);
}
}
/**
* {#inheritDoc}
*/
public function write($id, $data)
{
// get table/column
$dbTable = $this->dbOptions['db_table'];
$dbDataCol = $this->dbOptions['db_data_col'];
$dbIdCol = $this->dbOptions['db_id_col'];
$dbTimeCol = $this->dbOptions['db_time_col'];
$dbUserIdCol = $this->dbOptions['db_user_id_col'];
//session data can contain non binary safe characters so we need to encode it
$encoded = base64_encode($data);
$userId = $this->context->isGranted('IS_AUTHENTICATED_REMEMBERED') ?
$this->context->getToken()->getUser()->getId() :
null
;
try {
$driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
if ('mysql' === $driver) {
// MySQL would report $stmt->rowCount() = 0 on UPDATE when the data is left unchanged
// it could result in calling createNewSession() whereas the session already exists in
// the DB which would fail as the id is unique
$stmt = $this->pdo->prepare(
"INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol, $dbUserIdCol) VALUES (:id, :data, :time, :user_id) " .
"ON DUPLICATE KEY UPDATE $dbDataCol = VALUES($dbDataCol), $dbTimeCol = VALUES($dbTimeCol)"
);
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
$stmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->bindParam(':user_id', $userId, \PDO::PARAM_STR);
$stmt->execute();
} elseif ('oci' === $driver) {
$stmt = $this->pdo->prepare("MERGE INTO $dbTable USING DUAL ON($dbIdCol = :id) ".
"WHEN NOT MATCHED THEN INSERT ($dbIdCol, $dbDataCol, $dbTimeCol, $dbUserIdCol) VALUES (:id, :data, sysdate, :user_id) " .
"WHEN MATCHED THEN UPDATE SET $dbDataCol = :data WHERE $dbIdCol = :id");
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
$stmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
$stmt->bindParam(':user_id', $userId, \PDO::PARAM_STR);
$stmt->execute();
} else {
$stmt = $this->pdo->prepare("UPDATE $dbTable SET $dbDataCol = :data, $dbTimeCol = :time WHERE $dbIdCol = :id");
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
$stmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->execute();
if (!$stmt->rowCount()) {
// No session exists in the database to update. This happens when we have called
// session_regenerate_id()
$this->createNewSession($id, $data);
}
}
} catch (\PDOException $e) {
throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e);
}
return true;
}
private function createNewSession($id, $data = '')
{
// get table/column
$dbTable = $this->dbOptions['db_table'];
$dbDataCol = $this->dbOptions['db_data_col'];
$dbIdCol = $this->dbOptions['db_id_col'];
$dbTimeCol = $this->dbOptions['db_time_col'];
$dbUserIdCol = $this->dbOptions['db_user_id_col'];
$userId = $this->context->isGranted('IS_AUTHENTICATED_REMEMBERED') ?
$this->context->getToken()->getUser()->getId() :
null
;
$sql = "INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol, $dbUserIdCol) VALUES (:id, :data, :time, :user_id)";
//session data can contain non binary safe characters so we need to encode it
$encoded = base64_encode($data);
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':id', $id, \PDO::PARAM_STR);
$stmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->bindParam(':user_id', $userId, \PDO::PARAM_STR);
$stmt->execute();
return true;
}
}
And configure session to use it:
# config.yml
framework:
session:
# ...
handler_id: session.storage.custom
parameters:
pdo.db_options:
db_table: session
db_id_col: session_id
db_data_col: session_value
db_time_col: session_time
db_user_id_col: session_user_id
services:
pdo:
class: PDO
arguments:
dsn: "mysql:host=%database_host%;dbname=%database_name%"
user: "%database_user%"
password: "%database_password%"
session.storage.custom:
class: Acme\DemoBundle\HttpFoundation\Session\Storage\Handler\UserIdPdoSessionHandler
arguments: [ #pdo, "%pdo.db_options%", #security.context ]

I'm not sure modifying session is a good idea, you can store session id(s) in user entity instead and delete those when needed. This way you can for example ensure only user can be logged in with only one session at a time.
The easiest way to accomplish it would be to use login listener.
Add sessionId field to user entity (or document or whatever persistance you use):
// Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User {
// ...
/**
* #ORM\Column(name="session_id", type="string")
*/
private $sessionId;
public function getSessionId() {
return $this->sessionId;
}
public function setSessionId($sessionId = null) {
$this->sessionId = $sessionId;
return $this;
}
}
And add a listener:
namespace Dbla\UserBundle\Listener;
use Symfony\Component\HttpFoundation\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class LoginListener
{
protected $doctrine;
protected $session;
public function __construct(Session $session, Registry $doctrine)
{
$this->doctrine = $doctrine;
$this->session = $session;
}
public function onLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if ($user) {
$user->setSessionId($this->session->getId());
$em = $this->doctrine->getEntityManager();
$em->persist($user);
$em->flush();
}
}
}
And add it as a service:
services:
acme_user.listsner.login:
class: Acme\UserBundle\Listener\LoginListener
arguments: [ #session, #doctrine ]
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onLogin }
Then you can simply remove session for users:
$users = []; // ... get user list
$sessionIds = array_map(function($user) {
return $user->getId();
});
if (count(sessionIds) > 0) {
$sql = 'DELETE FROM session WHERE session_id IN (' . implode($sessionIds, ',') . ')';
$entityManager->getConnection()->exec($sql);
}
foreach ($users as $user) {
$user->setSessionId(null);
$entityManager->persist($user);
}
$entityManager->flush();

Related

Customize Laravel 8 Passport methods

The login validation I use is not Laravel's default. How do I customize Passport methods?
The following code I use to validate with web middleware.
$username = $request->username;
$password = strtoupper(md5($request->password));
$system = env("CODE_SYSTEM", 12);
$sql = "SELECT user.validate( '$system' , '$username', '$password')";
$stmt = DB::select(DB::raw($sql));
$result = $stmt[0]->validation;
if ($result == "ok") {
$user = new User();
$id = DB::table('users')->select('id')
->where('username' , $username)->first();
$user = User::find($id->id);
return $user;
}
To customize you need add the validateForPassportPasswordGrant() at User method, example:
public function validateForPassportPasswordGrant($password)
{
$password = strtoupper(md5($request->password));
$system = env("CODE_SYSTEM", 12);
$sql = "SELECT user.validate( '$system' , '$this->username', '$password')";
$stmt = DB::select(DB::raw($sql));
$result = $stmt[0]->validation;
if ($result == "ok") {
return true;
}
return false;
}
If you need to change the user column name where the passport search username:
public function findForPassport($username)
{
return $this->where('the_username_column', $username)->first();
}

How to refresh an entity load with a ServiceEntityRepository

I have load an entity using a Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository (new recommandation in SF4) and I want to refresh this entity.
But $entityManager->clear() don't do the job and
$entityManager->refresh($myentity)
told me
Doctrine\ORM\ORMInvalidArgumentException: Entity App\Entity\MyEntity#0000000068ed8bf0000000007dca9dd1 is not managed. An entity is managed if its fetched from the database or registered as new through EntityManager#persist
Here is my repo :
<?php
namespace App\Core\Repository;
use App\Core\Entity\Question;
use App\Core\Entity\QuestionnaireResponse;
use App\Core\Entity\Reponse;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;
/**
* #method Reponse|null find($id, $lockMode = null, $lockVersion = null)
* #method Reponse|null findOneBy(array $criteria, array $orderBy = null)
* #method Reponse[] findAll()
* #method Reponse[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ReponseRepository extends ServiceEntityRepository
{
/**
* ReponseRepository constructor.
* #param RegistryInterface $registry
*/
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Reponse::class);
}
}
Here is my testSetup :
<?php
/*
* Created by Aurelien Jolivard on 24/01/2019.
*/
namespace App\Core\Service;
use Doctrine\Bundle\DoctrineBundle\Command\DropDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand;
use Doctrine\Bundle\DoctrineBundle\Command\Proxy\CreateSchemaDoctrineCommand;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManager;
use Symfony\Bridge\Doctrine\ContainerAwareEventManager;
use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
class TestSetUp extends WebTestCase
{
/** #var EntityManager $em */
protected $em;
protected $application;
public function beginningTest(String $fixtures){
static::$kernel = static::createKernel(array(
'environment' => 'test',
'debug' => 'true'
));
static::$kernel->boot();
$application = new Application(static::$kernel);
// get the Entity Manager
/** #var EntityManager $em */
$em = static::$kernel->getContainer()
->get('doctrine')
->getManager();
/** #var ContainerAwareEventManager $evm */
$evm = $em->getEventManager();
// drop the database
$command = new DropDatabaseDoctrineCommand(static::$kernel->getContainer()->get('doctrine'));
$application->add($command);
$input = new ArrayInput(array(
'command' => 'doctrine:database:drop',
'--force' => true,
'--env' => 'test'
));
$command->run($input, new NullOutput());
// we have to close the connection after dropping the database so we don't get "No database selected" error
$connection = $application->getKernel()->getContainer()->get('doctrine')->getConnection();
if ($connection->isConnected()) {
$connection->close();
}
// create the database
$command = new CreateDatabaseDoctrineCommand(static::$kernel->getContainer()->get('doctrine'));
$application->add($command);
$input = new ArrayInput(array(
'command' => 'doctrine:database:create',
'--env' => 'test'
));
$command->run($input, new NullOutput());
// create schema
$command = new CreateSchemaDoctrineCommand();
$application->add($command);
$input = new ArrayInput(array(
'command' => 'doctrine:schema:create',
'--env' => 'test'
));
$command->run($input, new NullOutput());
// load fixtures
$client = static::createClient();
$loader = new ContainerAwareLoader($client->getContainer());
$loader->loadFromFile(static::$kernel->getProjectDir().$fixtures);
$purger = new ORMPurger($em);
$executor = new ORMExecutor($em, $purger);
$executor->execute($loader->getFixtures());
$this->em = $em;
$this->application = $application;
return ;
}
}
And here is my test where i try to clear data.
<?php
namespace App\Core\Tests\Form\FunctionalTest;
use App\Core\Entity\Questionnaire;
use App\Core\Entity\QuestionnaireResponse;
use App\Core\Entity\Reponse;
use App\Core\Service\TestSetUp;
use Doctrine\ORM\EntityManager;
class QuestionTypeBooleanTest extends TestSetUp
{
public function setUp()
{
$array = $this->beginningTest('/src/Core/DataFixtures/AppFixturesQuestionTypeBoolean.php');
//$this->em = $array[0];
//$this->application = $array[1];
}
/**
* #group question
* #group test_BrouillonPuisSoumission
*/
public function test_BrouillonPuisSoumission()
{
// vérification de l'état de la base de données avant
$client = static::createClient(
array('environment' => 'test') ,
);
$questionnaire = $this->em->getRepository(Questionnaire::class)->findSystemAndValue('system', '123');
$crawler = $client->request(
'GET',
'/questionnaire/'.$questionnaire->getId().'/display/1'
);
$response = $client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
$form = $crawler->selectButton('appbundle_questionnaire_display[draft]')->form();
$form['appbundle_questionnaire_display[1]'] = 0;
$form['appbundle_questionnaire_display[2]'] = 1;
$crawler = $client->submit($form);
$response = $client->getResponse();
$this->assertEquals(302, $response->getStatusCode());
// vérification de l'état de la base de données
$reponse1 = $this->em->getRepository(Reponse::class)->find(1);
$reponse2 = $this->em->getRepository(Reponse::class)->find(2);
$questionnaireResponse = $this->em->getRepository(QuestionnaireResponse::class)->find(1);
$this->assertEquals('0', $reponse1->getValeur(), 'cas1.1');
$this->assertEquals('1', $reponse2->getValeur(), 'cas1.2');
$this->assertEquals(2, $questionnaireResponse->getVersionId());
$crawler = $client->request(
'GET',
'/soumission/1/edit'
);
$response = $client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(0, $crawler->filter('form[name="appbundle_questionnaire_display"]')->form()->getValues()['appbundle_questionnaire_display[1]'], 'cas2.1');
$this->assertEquals(1, $crawler->filter('form[name="appbundle_questionnaire_display"]')->form()->getValues()['appbundle_questionnaire_display[2]'], 'cas2.2');
$form = $crawler->selectButton('appbundle_questionnaire_display[submit]')->form();
$form['appbundle_questionnaire_display[1]'] = 1;
$form['appbundle_questionnaire_display[2]'] = 0;
$crawler = $client->submit($form);
$response = $client->getResponse();
$this->assertEquals(302, $response->getStatusCode());
$this->em->refresh($reponse1);
$this->em->refresh($reponse2);
$this->em->refresh($questionnaireResponse);
//$this->em->clear();
$this->assertEquals('1', $this->em->getRepository(Reponse::class)->find(1)->getValeur(), 'cas3.1');
$this->assertEquals('0', $this->em->getRepository(Reponse::class)->find(2)->getValeur(), 'cas3.2');
$this->assertEquals(3, $this->em->getRepository(QuestionnaireResponse::class)->find(1)->getVersionId());
}
}
How can I do that ?
Doctrine/ORM/EntityManager::clear() method
Clears the EntityManager. All entities that are currently managed by this EntityManager become detached.
Doctrine/ORM/EntityManager::refresh() method
Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been persisted.
To refresh the entity, just do:
$entityManager->refresh($myentity)
without clear() before it, remove it.
I have found a solution.
The entityManager i get in the TestSetUp is not the one used by the ServiceEntityRepository. This is due to the construct of the ServiceEntityRepository.
Si to clear the good entityManager, i have to call clear on the Repository and not on my EntityManger :
public function test_BrouillonPuisSoumission()
{
// ...
// vérification de l'état de la base de données
$reponseRepository = $this->em->getRepository(Reponse::class);
$questionnaireResponseRepository = $this->em->getRepository(QuestionnaireResponse::class);
$this->em->getRepository(QuestionnaireResponse::class)->find(1)->getVersionId());
$this->assertEquals('0', $reponseRepository->find(1)->getValeur(), 'cas1.1');
$this->assertEquals('1', $reponseRepository->find(2)->getValeur(), 'cas1.2');
$this->assertEquals(2, $questionnaireResponseRepository->find(1)->getVersionId());
// ...
$reponseRepository->clear();
$questionnaireResponseRepository->clear();
$this->assertEquals('1', $reponseRepository->find(1)->getValeur(), 'cas3.1');
$this->assertEquals('0', $reponseRepository->find(2)->getValeur(), 'cas3.2');
$this->assertEquals(3, $questionnaireResponseRepository->find(1)->getVersionId());
}

Can't filter data from database using getSelect()->where() Magento 2

I am currently working with magento 2.2.1 and I am having a weird problem. I am trying to get a set of data from database and display it on admin grid. I want to take records for a specific agent ID so i have a variable that has the value of the agent id. When i pass this variable as parameter to$this->collection->getSelect()->where('agent_id = ?', $this->givenAgentId); it wont display anything but if i replace $this->givenAgentId with it's value, for example with 4, it works perfectly!
This is my class:
<?php
namespace vendor\plugin\Ui\Component\Listing\DataProviders\Atisstats\Coupons;
use \vendor\plugin\Model\ResourceModel\Coupons\CollectionFactory;
use \Magento\Framework\Registry;
class Listing extends \Magento\Ui\DataProvider\AbstractDataProvider {
protected $_registry;
protected $givenAgentId = 0;
public function __construct(
Registry $registry,
$name,
$primaryFieldName,
$requestFieldName,
CollectionFactory $collectionFactory,
array $meta = [],
array $data = []
) {
parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
$this->_registry = $registry;
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$resource = $objectManager->get('\Magento\Framework\App\ResourceConnection');
$connection = $resource->getConnection();
$select = $connection->select()
->from(
['amasty_perm_dealer'],
['user_id']
);
$data = $connection->fetchAll($select);
foreach ($data as $dealerId) {
if ($dealerId['user_id'] == $this->_registry->registry('admin_session_id')) {
$this->givenAgentId = intval($this->_registry->registry('admin_session_id'));
}
}
if ($this->givenAgentId != 0) {
$this->collection->getSelect()->where('agent_id = ?', $this->givenAgentId);
} else {
$this->collection = $collectionFactory->create();
}
}
}
I have stuck here for hours!
I fixed this problem! First of all it was Registry class causing the problem so I used
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$resourceUserId = $objectManager->get('\Magento\Backend\Model\Auth\Session');
to get the user id from session and used it below to check the user! For some reason the registry object was modifying the variable holding the current users id!
I post the answer just in case someone get stuck with this kind of problem !

Magento. How to link store_id to the attribute in the custom EAV Model

I am using this tutorial on adding new EAV Model in Magento:
http://inchoo.net/ecommerce/magento/creating-an-eav-based-models-in-magento/
Everything works fine except all my attributes are saving with "store_id = 0" when I do this part of code:
$phonebookUser = Mage::getModel('inchoo_phonebook/user');
$phonebookUser->setFristname('John');
$phonebookUser->save();
I am wondering is there any clear way to set store ID on save EAV Entity Attributes.
Thanks.
You can only set values for a specific store only after you added the values for store id 0.
Here is an example.
//create default values
$phonebookUser = Mage::getModel('inchoo_phonebook/user');
$phonebookUser->setFristname('John');
$phonebookUser->save();
//remember the id of the entity just created
$id = $phonebookUser->getId();
//update the name for store id 1
$phonebookUser = Mage::getModel('inchoo_phonebook/user')
->setStoreId(1)
->load($id); //load the entity for a store id.
$phonebookUser->setFristname('Jack'); //change the name
$phonebookUser->save(); //save
I have override the functions in my resource model to work with store_id and it is worked for me but I suggest that this is not the best solution.
protected function _saveAttribute($object, $attribute, $value)
{
$table = $attribute->getBackend()->getTable();
if (!isset($this->_attributeValuesToSave[$table])) {
$this->_attributeValuesToSave[$table] = array();
}
$entityIdField = $attribute->getBackend()->getEntityIdField();
$data = array(
'entity_type_id' => $object->getEntityTypeId(),
$entityIdField => $object->getId(),
'attribute_id' => $attribute->getId(),
'store_id' => $object->getStoreId(), //added this
'value' => $this->_prepareValueForSave($value, $attribute)
);
$this->_attributeValuesToSave[$table][] = $data;
return $this;
}
protected function _getLoadAttributesSelect($object, $table)
{
$select = $this->_getReadAdapter()->select()
->from($table, array())
->where($this->getEntityIdField() . ' =?', $object->getId())
->where('store_id in (?)', array($object->getStoreId(), 0)); //added this
return $select;
}
also I have added this code to the constructor of my entity model:
if (Mage::app()->getStore()->isAdmin()) {
$this->setStoreId(Mage::app()->getRequest()->getParam('store', 0));
}
else{
$this->setStoreId(Mage::app()->getStore()->getId());
}
Override the _getDefaultAttributes() method in your resource model like this:
protected function _getDefaultAttributes()
{
$attributes = parent::_getDefaultAttributes();
$attributes[] = "store_id";
return $attributes;
}
This should work if you have only one value for store_id per your model's entity.

Get Full Billing Address for Paypal Express [Magento]

The paypal module tries to map the billing information that is returned (usually nothing) from Paypal over the billing information entered by the user during the checkout process. I've fond the code that does this in NVP.php model.
/**
* Create billing and shipping addresses basing on response data
* #param array $data
*/
protected function _exportAddressses($data)
{
$address = new Varien_Object();
Varien_Object_Mapper::accumulateByMap($data, $address, $this->_billingAddressMap);
$address->setExportedKeys(array_values($this->_billingAddressMap));
$this->_applyStreetAndRegionWorkarounds($address);
$this->setExportedBillingAddress($address);
// assume there is shipping address if there is at least one field specific to shipping
if (isset($data['SHIPTONAME'])) {
$shippingAddress = clone $address;
Varien_Object_Mapper::accumulateByMap($data, $shippingAddress, $this->_shippingAddressMap);
$this->_applyStreetAndRegionWorkarounds($shippingAddress);
// PayPal doesn't provide detailed shipping name fields, so the name will be overwritten
$shippingAddress->addData(array(
'prefix' => null,
'firstname' => $data['SHIPTONAME'],
'middlename' => null,
'lastname' => null,
'suffix' => null,
));
$this->setExportedShippingAddress($shippingAddress);
}
}
/**
* Adopt specified address object to be compatible with Magento
*
* #param Varien_Object $address
*/
protected function _applyStreetAndRegionWorkarounds(Varien_Object $address)
{
// merge street addresses into 1
if ($address->hasStreet2()) {
$address->setStreet(implode("\n", array($address->getStreet(), $address->getStreet2())));
$address->unsStreet2();
}
// attempt to fetch region_id from directory
if ($address->getCountryId() && $address->getRegion()) {
$regions = Mage::getModel('directory/country')->loadByCode($address->getCountryId())->getRegionCollection()
->addRegionCodeFilter($address->getRegion())
->setPageSize(1)
;
foreach ($regions as $region) {
$address->setRegionId($region->getId());
$address->setExportedKeys(array_merge($address->getExportedKeys(), array('region_id')));
break;
}
}
}
Has anyone had any success modifying this process to get back fuller billing information. We need to be able to send "Paid" invoices to customers who pay with Paypal, so we need to capture this information.
i have hacked a workaround for that problem. It is not the cleanest way but i can save the billing address data in the order after paying with PayPal. I spent 2 days working on it and at the end i coded only a few lines. I marked my workaround with the comment #103.
Override method of class Mage_Paypal_Model_Api_Nvp:
protected function _importAddresses(array $to)
{
// Original Code
//$billingAddress = ($this->getBillingAddress()) ? $this->getBillingAddress() : $this->getAddress();
// Workaround #103
if ($this->getBillingAddress())
{
$billingAddress = $this->getBillingAddress();
}
else
{
$chkout = Mage::getSingleton('checkout/session');
$quote = $chkout->getQuote();
$billingAddress = $quote->getBillingAddress();
$billingAddress->setData($billingAddress->getOrigData());
$session = Mage::getSingleton("core/session", array("name"=>"frontend"));
$session->setData("syn_paypal_original_billing_address", serialize($billingAddress->getOrigData()));
}
$shippingAddress = $this->getAddress();
$to = Varien_Object_Mapper::accumulateByMap($billingAddress, $to, array_flip($this->_billingAddressMap));
if ($regionCode = $this->_lookupRegionCodeFromAddress($billingAddress)) {
$to['STATE'] = $regionCode;
}
if (!$this->getSuppressShipping()) {
$to = Varien_Object_Mapper::accumulateByMap($shippingAddress, $to, array_flip($this->_shippingAddressMap));
if ($regionCode = $this->_lookupRegionCodeFromAddress($shippingAddress)) {
$to['SHIPTOSTATE'] = $regionCode;
}
$this->_importStreetFromAddress($shippingAddress, $to, 'SHIPTOSTREET', 'SHIPTOSTREET2');
$this->_importStreetFromAddress($billingAddress, $to, 'STREET', 'STREET2');
$to['SHIPTONAME'] = $shippingAddress->getName();
}
$this->_applyCountryWorkarounds($to);
return $to;
}
And override method in Mage_Paypal_Model_Express_Checkout:
public function returnFromPaypal($token)
{
$this->_getApi();
$this->_api->setToken($token)
->callGetExpressCheckoutDetails();
// import billing address
$billingAddress = $this->_quote->getBillingAddress();
$exportedBillingAddress = $this->_api->getExportedBillingAddress();
// Workaround #103
$session = Mage::getSingleton("core/session", array("name"=>"frontend"));
$dataOrg = unserialize($session->getData("syn_paypal_original_billing_address"));
if (true === is_object($billingAddress))
{
foreach ($exportedBillingAddress->getExportedKeys() as $key) {
if (array_key_exists($key, $dataOrg))
{
$billingAddress->setData($key, $dataOrg[$key]);
}
}
$this->_quote->setBillingAddress($billingAddress);
}
// import shipping address
$exportedShippingAddress = $this->_api->getExportedShippingAddress();
if (!$this->_quote->getIsVirtual()) {
$shippingAddress = $this->_quote->getShippingAddress();
if ($shippingAddress) {
if ($exportedShippingAddress) {
foreach ($exportedShippingAddress->getExportedKeys() as $key) {
$shippingAddress->setDataUsingMethod($key, $exportedShippingAddress->getData($key));
}
$shippingAddress->setCollectShippingRates(true);
}
// import shipping method
$code = '';
if ($this->_api->getShippingRateCode()) {
if ($code = $this->_matchShippingMethodCode($shippingAddress, $this->_api->getShippingRateCode())) {
// possible bug of double collecting rates :-/
$shippingAddress->setShippingMethod($code)->setCollectShippingRates(true);
}
}
$this->_quote->getPayment()->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_SHIPPING_METHOD, $code);
}
}
$this->_ignoreAddressValidation();
// import payment info
$payment = $this->_quote->getPayment();
$payment->setMethod($this->_methodType);
Mage::getSingleton('paypal/info')->importToPayment($this->_api, $payment);
$payment->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_PAYER_ID, $this->_api->getPayerId())
->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_TOKEN, $token)
;
$this->_quote->collectTotals()->save();
}

Resources