ManyToOne - why it create duplicate foreign object / Traget Entity - symfony

This is my database, as you can see, I have two Many To One relation and the owner of relationship is the table ESA.
For this web app, I use Symfony 4 and doctrine as ORM, and MySQL 5.7.24, PHP 7.2 .
(https://imgur.com/oCzzs2a)
The process is :
I upload as csv
Create a row in database table filesupload
with mention of this csv
Read the csv
Import each row of csv into the ESA table and set id_filesupload field with the filesupload object
I try to use the :
$entityManager->merge();
Working greate but only when id_filesupload has been already set in the table ESA.
It's doesn't create duplicate filesupload row with same value.
It duplicate my filesupload everytime I flush.
I have try to no pass the object filesupload to the import function but only the id, and get the object by the id.. the result is the same.
In a other hand, the exactly the same process for Department and it doesn't create duplicate entries into Department table.
Part of my Entity ESA
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Esa
*
* #ORM\Table(name="esa")
* #ORM\Entity(repositoryClass="App\Repository\EsaRepository")
*/
class Esa
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Department", inversedBy="Esa")
* #ORM\JoinColumn(name="department_id", referencedColumnName="id", nullable=true)
*/
private $department;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Filesupload", inversedBy="Esa")
* #ORM\JoinColumn(name="filesupload_id", referencedColumnName="id", nullable=true)
*/
private $filesupload;
public function getDepartment(): ?Department
{
return $this->department;
}
public function setDepartment(?Department $department): self
{
$this->department = $department;
return $this;
}
public function getFilesupload(): ?Filesupload
{
return $this->filesupload;
}
public function setFilesupload(?Filesupload $filesupload): self
{
$this->filesupload = $filesupload;
return $this;
}
}
Part of my Controller ESA Upload the CSV (Process step 1 + 2)
/**
* #Route("/Aqueduct/UploadData", name="Aqueduct_Upload")
*/
public function UploadData(Request $request)
{
$entityManager = $this->getDoctrine()->getManager();
$form = $this->createForm(FilesuploadType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$FilesuploadFile = $form['UploaderESA']->getData();
// this condition is needed because the 'ESA csv' field is not required
// so the CSV file must be processed only when a file is uploaded
if ($FilesuploadFile) {
$originalFilename = pathinfo($FilesuploadFile->getClientOriginalName(), PATHINFO_FILENAME);
// this is needed to safely include the file name as part of the URL
$safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
//$newFilename = $safeFilename.'-'.uniqid().'.'.$brochureFile->guessExtension();
//force csv
$newFilename = $safeFilename.'-'.uniqid().'.csv';
// Move the file to the directory where csv are stored
try {
//get the new param of moving file
$FilesuploadFile=$FilesuploadFile->move(
$this->getParameter('uploads_directory'),
$newFilename
);
// create and set this Fileupload
$FileUpload = new Filesupload();
$FileUpload
->setType("ESA")
->setFilename($newFilename);
// save the uploaded filename to database
$entityManager->persist($FileUpload);
$entityManager->flush();
$entityManager->clear();
} catch (FileException $e) {
// ... handle exception if something happens during file upload
}
}
$this->ImportESA($FilesuploadFile, $FileUpload);
}
else{
return $this->render('Aqueduct/import.html.twig', [ 'form' => $form->createView()]);
}
}
Part of my Controller ESA Upload the CSV (Process step 3 + 4)
public function ImportESA($FilesuploadFile, $FileUpload)
{
$batchSize = 80;
$i=0;
$entityManager = $this->getDoctrine()->getManager();
$repositoryESA = $this->getDoctrine()->getRepository(Esa::class);
$csv = Reader::createFromPath($FilesuploadFile->getRealPath(), 'r');
//param the header of the array
$csv->setDelimiter(';');
$csv->setEnclosure('"');
$csv->setHeaderOffset(0);
/*$csv->setEncodingFrom('iso-8859-15');*/
$records = $csv->getRecords();
foreach ($records as $offset => $record) {
//Remove matu and degree and class split
$classLetter = $this->RemoveMatuTag($this->AllLettersBeforeNumb($record["Classe - Nom"]));
$department = $this->GetDepartmentByClasseName($classLetter);
++$i;
$EsaRecord = new Esa();
$EsaRecord
->setDepartment($department)
->setConcatenate($Concatenate)
->setFilesupload($FileUpload)
;
$entityManager->persist($EsaRecord);
if (($i % $batchSize) === 0) {
$entityManager->flush();
$message = 'Done';
$entityManager->clear(); // Detaches all objects from Doctrine!
}
}
}
}
$entityManager->flush();
$entityManager->clear(); // Detaches all objects from Doctrine!
return $this->redirect($this->generateUrl('Aqueduct_TransformData'));
}
How I get the department
public function AllLettersBeforeNumb($var)
{
return preg_replace("/\d.*/","$2",$var);
}
public function RemoveMatuTag($var)
{
return str_replace(" MATU", "",$var);
}
public function GetDepartmentByClasseName($var)
{
$repository = $this->getDoctrine()->getRepository(Education::class);
$education = $repository->findOneBy(['Shorten' => $var]);
$department = NULL;
if ($education != NULL) {
$department = $education->getDepartment();
}
if (! $department){
$repository = $this->getDoctrine()->getRepository(Department::class);
$department = $repository->find(0);
}
return $department;
}
As my understanding I don't want to : cascade={"persist"} cause it will create an filesupload row for each row in my CSV (ESA table).
I expect the have only 1 filesupload row for all my new esa row.
But actual I have 1 filesupload for each packet of 80 lines, cause of $entityManager->flush();
I have 17160 row in my csv.

It's cause I unset the $FileUpload when I call the clear() method of the entity manager.
My bad...

Related

How to close DB connections and reduce their number in functional PHPUnit tests for a ZF3 application?

Some years ago I already created a very similar or even nearly the same question on StackOverflow. I got great detailed answers, but they didn't lead to a solution of my problem. Now, the problem became even bigger and I'm starting the second attempt to solve this here. Since the code has been changed, I don't want to update the original question. It would be a too big update and the answers might perhaps not match to the new version of the question. So I'm formulating it as a new one:
I'm writing functional tests for a Zend Framework 3 application by using
zendframework/zend-test 3.2.2,
phpunit/phpunit 6.5.14, and
phpunit/dbunit 3.0.3
The most of the tests are a kind of controller tests. The test code calls a URI / an action by using the Zend\Test\PHPUnit\Controller\AbstractControllerTestCase#dispatch(...) and analyzes 1. the response / output data and 2. the changes at the database (if it was a writing call like "create foo"), e.g.:
/**
...
* #dataProvider provideDataForShowOrderAccess
*/
public function testShowOrderAccess(string $username, int $responseStatusCode)
{
...
$this->createOrder(...);
$this->reset();
$_SERVER['AUTH_USER'] = $username;
...
$this->dispatch($showUrl);
$this->assertResponseStatusCode($responseStatusCode);
}
/**
...
* #dataProvider provideDataForShowOrder
*/
public function testShowOrder(string $username, bool $isOwner)
{
...
$this->createOrder($connectionType, $endpointSourceType);
$this->reset();
$_SERVER['AUTH_USER'] = $username;
// testing the access by the owner
$orderId = 1;
$showUrl = '/order/show/' . $orderId;
$this->dispatch($showUrl);
if ($isOwner) {
$this->assertResponseStatusCode(Response::STATUS_CODE_200);
$this->assertModuleName('Order');
$this->assertControllerName('Order\Controller\Process');
$this->assertControllerClass('ProcessController');
$this->assertMatchedRouteName('order/show');
/** #var Foo $foo */
$foo = $this->getApplication()->getMvcEvent()->getResult()->getVariable('foo', null);
$fooData = $createParams['foo'];
$barData = $barData['bar'];
$this->assertNotNull($bar);
$this->assertInstanceOf(Foo::class, $foo);
$this->assertEquals($orderId, $foo->getId());
$this->assertEquals($fooData['bar'], $foo->getBar());
...
} else {
$this->assertResponseStatusCode(Response::STATUS_CODE_302);
}
}
For every single test the database gets reset.
The problem is, that the number of database connections is growing and growing and growing -- with every next test. Currently there are about 350 (SHOW GLOBAL STATUS LIKE 'max_used_connections';) connections for 102 tests. (As a workaround I have to increase the MySQL's max_connections more and more.)
I tried to decrease the number of connections by putting logic like $this->dbAdapter->getDriver()->getConnection()->disconnect(); or/and $this->entityManager->getConnection()->close(); to the tearDown() of my super-class for controller tests. This way I got the number of the connections reduced by about 90. But the most of the connections still don't get killed.
How to close DB connections and significantly reduce the number of concurrently open connections in functional / controller PHPUnit tests for a ZF3 application?
Additional information: the most relevant parts of my code
AbstractControllerTest
namespace Base\Test;
use Doctrine\ORM\EntityManager;
use PDO;
use PHPUnit\DbUnit\Database\DefaultConnection;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Sql\Sql;
use Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;
/**
* Class AbstractControllerTest
*
* #package Base\Test
*/
abstract class AbstractControllerTest extends AbstractHttpControllerTestCase
{
use DatabaseConnectionTrait;
/**
* #var string
*/
static private $applicationConfigPath;
/** #var Adapter */
protected $dbAdapter;
/** #var EntityManager */
protected $entityManager;
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
$this->setApplicationConfig(include self::$applicationConfigPath);
}
public static function setApplicationConfigPath(string $applicationConfigPath)
{
self::$applicationConfigPath = $applicationConfigPath;
}
protected function tearDown()
{
// Connections: 354
// Time: 5.7 minutes, Memory: 622.00MB
// OK (102 tests, 367 assertions)
// no optimization
// Connections: 326 (26 connections less)
// Time: 5.86 minutes, Memory: 620.00MB
// OK (102 tests, 367 assertions)
// if ($this->dbAdapter && $this->dbAdapter instanceof Adapter) {
// $this->dbAdapter->getDriver()->getConnection()->disconnect();
// }
// Connections: 354
// Time: 5.67 minutes, Memory: 620.00MB
// OK (102 tests, 367 assertions)
// $this->entityManager->close();
// Connections: 291 (63 connections less)
// Time: 5.63 minutes, Memory: 622.00MB
// OK (102 tests, 367 assertions)
// $this->entityManager->getConnection()->close();
// Connections: 264 (90 connections less)
// Time: 5.7 minutes, Memory: 620.00MB
// OK (102 tests, 367 assertions)
// if ($this->dbAdapter && $this->dbAdapter instanceof Adapter) {
// $this->dbAdapter->getDriver()->getConnection()->disconnect();
// }
// $this->entityManager->getConnection()->close();
// Connections: 251
// Time: 4.71 minutes, Memory: 574.00MB
// OK (102 tests, 367 assertions)
// After removing initialization of the EntityManager and the DbAdapter in the constructor and the setUp().
// closing DB connections
if ($this->dbAdapter && $this->dbAdapter instanceof Adapter) {
$this->dbAdapter->getDriver()->getConnection()->disconnect();
}
if ($this->entityManager && $this->entityManager instanceof EntityManager) {
$this->entityManager->getConnection()->close();
}
$reflectionObject = new \ReflectionObject($this);
foreach ($reflectionObject->getProperties() as $prop) {
if (!$prop->isStatic() && 0 !== strpos($prop->getDeclaringClass()->getName(), 'PHPUnit_')) {
$prop->setAccessible(true);
$prop->setValue($this, null);
}
}
$this->reset();
$this->application = null;
gc_collect_cycles();
unset($_SERVER['AUTH_USER']);
parent::tearDown();
}
protected function retrieveActualData($table, $idColumn, $idValue)
{
$sql = new Sql($this->getDbAdapter());
$select = $sql->select($table);
$select->where([$table . '.' . $idColumn . ' = ?' => $idValue]);
$statement = $sql->prepareStatementForSqlObject($select);
$result = $statement->execute();
$data = $result->current();
// Decreases the total number of the connections by 1 less.
// $this->dbAdapter->getDriver()->getConnection()->disconnect();
return $data;
}
protected function getEntityManager()
{
$this->entityManager = $this->entityManager
?: $this->getApplicationServiceLocator()->get('doctrine.entitymanager.orm_default')
;
return $this->entityManager;
}
protected function getDbAdapter()
{
$this->dbAdapter = $this->dbAdapter
?: $this->getApplicationServiceLocator()->get('Zend\Db\Adapter\Adapter')
;
return $this->dbAdapter;
}
}
DatabaseConnectionTrait
namespace Base\Test;
use PDO;
use PHPUnit\DbUnit\Database\Connection;
use PHPUnit\DbUnit\Database\DefaultConnection;
use PHPUnit\DbUnit\InvalidArgumentException;
trait DatabaseConnectionTrait
{
/**
* #var array
*/
static private $dbConfigs;
/**
* #var PDO
*/
static private $pdo;
/**
* #var Connection
*/
private $connection;
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
}
/**
* #return Connection
*/
public function getConnection()
{
if (! $this->connection) {
if (! self::$dbConfigs) {
throw new InvalidArgumentException(
'Set the database configuration first.'
. ' '. 'Use the ' . self::class . '::setDbConfigs(...).'
);
}
if (! self::$pdo) {
self::$pdo = new PDO(
self::$dbConfigs['dsn'],
self::$dbConfigs['username'],
self::$dbConfigs['password'],
[PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'']
);
}
$this->connection = $this->createDefaultDBConnection(self::$pdo, self::$dbConfigs['database']);
}
return $this->connection;
}
public static function setDbConfigs(array $dbConfigs)
{
self::$dbConfigs = $dbConfigs;
}
/**
* Creates a new DefaultDatabaseConnection using the given PDO connection
* and database schema name.
*
* #see The original PHPUnit\DbUnit\TestCaseTrait#createDefaultDBConnection(...).
*
* #param PDO $connection
* #param string $schema
*
* #return DefaultConnection
*/
protected function createDefaultDBConnection(PDO $connection, $schema = '')
{
return new DefaultConnection($connection, $schema);
}
}
DatabaseInitializer
namespace Base\Test;
/**
* Class DatabaseInitializer
*
* #package Base\Test
*/
class DatabaseInitializer
{
use DatabaseConnectionTrait;
/**
* #var string
*/
private $database;
public function __construct(array $dbConfigs)
{
$this->database = $dbConfigs['database'];
self::$dbConfigs = $dbConfigs;
}
public function setUp()
{
$schemaSql = file_get_contents(self::$dbConfigs['scripts']['schema']);
$storedProceduresSql = file_get_contents(self::$dbConfigs['scripts']['stored-procedures']);
$basicDataSql = file_get_contents(self::$dbConfigs['scripts']['basic-data']);
$testDataSqlSet = array_map(function ($sqlFile) {
return file_get_contents($sqlFile);
}, self::$dbConfigs['scripts']['test-data']);
$this->dropDatabase();
$this->createDatabase();
$this->useDatabase();
$this->createSchema($schemaSql);
$this->createStoredProcedures($storedProceduresSql);
$this->createBasicData($basicDataSql);
$this->createTestData($testDataSqlSet);
}
public function tearDown()
{
self::$pdo = null;
}
protected function createDatabase()
{
$this->getDatabaseConnection()->exec('CREATE DATABASE IF NOT EXISTS ' . $this->database . ';');
}
protected function useDatabase()
{
$this->getDatabaseConnection()->exec('USE ' . $this->database . ';');
}
protected function createSchema(string $sql)
{
$this->getDatabaseConnection()->exec($sql);
}
protected function createBasicData(string $sql)
{
$this->getDatabaseConnection()->exec($sql);
}
protected function createTestData(array $sqlSet = [])
{
foreach ($sqlSet as $sql) {
$this->getDatabaseConnection()->exec($sql);
}
}
protected function createStoredProcedures(string $sql)
{
$statement = $this->getDatabaseConnection()->prepare($sql);
$statement->execute();
}
protected function dropDatabase()
{
$this->getDatabaseConnection()->exec('DROP DATABASE IF EXISTS ' . $this->database . ';');
}
protected function getDatabaseConnection()
{
return $this->getConnection()->getConnection();
}
}
Bootstrap
namespace Test;
use Base\Test\AbstractControllerTest;
use Base\Test\AbstractDbTest;
use Base\Test\DatabaseInitializer;
use Doctrine\ORM\EntityManager;
use RuntimeException;
use Zend\Loader\AutoloaderFactory;
use Zend\Mvc\Service\ServiceManagerConfig;
use Zend\ServiceManager\ServiceManager;
error_reporting(E_ALL | E_STRICT);
ini_set('memory_limit', '2048M');
chdir(__DIR__);
/**
* Sets up the MVC (application, service manager, autoloading) and the database.
*/
class Bootstrap
{
/** #var ServiceManager */
protected $serviceManager;
protected $applicationConfigPath;
/** #var EntityManager */
protected $entityManager;
public function __construct()
{
$this->applicationConfigPath = __DIR__ . '/../config/application.config.php';
}
/**
* Sets up the
*/
public function init()
{
// autoloading setup
static::initAutoloader();
// application configuration & setup
$applicationConfig = require_once $this->applicationConfigPath;
$this->prepareApplication($applicationConfig);
// database configuration & setup
$dbConfigs = $this->serviceManager->get('Config')['db'];
$this->setUpDatabase($dbConfigs);
// listeners & application bootstrap
$listeners = $this->prepareListeners();
$this->bootstrapApplication($listeners);
}
public function chroot()
{
$rootPath = dirname(static::findParentPath('module'));
chdir($rootPath);
}
protected function prepareApplication($config)
{
$serviceManagerConfig = isset($config['service_manager']) ? $config['service_manager'] : [];
$serviceManagerConfigObject = new ServiceManagerConfig($serviceManagerConfig);
$this->serviceManager = new ServiceManager();
$serviceManagerConfigObject->configureServiceManager($this->serviceManager);
$this->serviceManager->setService('ApplicationConfig', $config);
$this->serviceManager->get('ModuleManager')->loadModules();
}
protected function prepareListeners()
{
$listenersFromAppConfig = [];
$config = $this->serviceManager->get('config');
$listenersFromConfigService = isset($config['listeners']) ? $config['listeners'] : [];
$listeners = array_unique(array_merge($listenersFromConfigService, $listenersFromAppConfig));
return $listeners;
}
protected function bootstrapApplication($listeners)
{
$application = $this->serviceManager->get('Application');
$application->bootstrap($listeners);
}
protected function setUpDatabase(array $dbConfigs)
{
$databaseInitializer = new DatabaseInitializer($dbConfigs);
$databaseInitializer->setUp();
AbstractDbTest::setDbConfigs($dbConfigs);
AbstractControllerTest::setApplicationConfigPath($this->applicationConfigPath);
AbstractControllerTest::setDbConfigs($dbConfigs);
}
protected function initAutoloader()
{
$vendorPath = static::findParentPath('vendor');
if (file_exists($vendorPath.'/autoload.php')) {
include $vendorPath.'/autoload.php';
}
if (! class_exists('Zend\Loader\AutoloaderFactory')) {
throw new RuntimeException(
'Unable to load ZF2. Run `php composer.phar install`'
);
}
AutoloaderFactory::factory(array(
'Zend\Loader\StandardAutoloader' => array(
'autoregister_zf' => true,
'namespaces' => array(
__NAMESPACE__ => __DIR__,
),
),
));
}
protected function findParentPath($path)
{
$dir = __DIR__;
$previousDir = '.';
while (!is_dir($dir . '/' . $path)) {
$dir = dirname($dir);
if ($previousDir === $dir) {
return false;
}
$previousDir = $dir;
}
return $dir . '/' . $path;
}
}
$bootstrap = new Bootstrap();
$bootstrap->init();
$bootstrap->chroot();
Since we don't see your controller, your repositories etc. we can't say you "there' mistake". Normally, you should make your connection for database test on setUp method, and destroy it on tearDown method. And you shouldn't start connection for application. Application should start that connection when it needs. Since you're using Doctrine, it does that. Why you're creating/calling EntityManager before tests?
What happens if you run only one test instead whole testcase? How many connections happens? Maybe you're making a mistake in a controller? In a repository maybe?
Here what i see when i look that code;
I couldn't see where initializer is using. Where do you using it? I didn't like that methods in it.
Your both DatabaseConnectionTrait and AbstractControllerTest classes have _construct method. If a class overwrites a method which is defined in trait, class' method works. Why trait has that method?
Are you sure you're not doing same thing over and over again?getDatabaseConnection, getConnection, $this->getConnection()->getConnection();
Where you define static $em property?
unset that reflection object when you done with it. I know it's easy and faster way to desctruct all properties, but you should consider to do it yourself. It'll be better way to manage your ram.
Why you trying to destruct all properties of class in tearDown method with reflection? Destroy connection and let php manage garbage.
Why you destroy database on tearDown method? If you provide your test-data to phpunit correctly, you don't need that. Check tearDownOperation of DbUnit.
I suggest you run that tests one by one instead whole test-case and check connection counts. Also you should test your Repositories with DbUnit to be sure they're working right. Since Repositories responsible on database, maybe that connection count increasing on them.

Symfony2 data transformer string to entity (reverseTransform)

I am pretty new to Symfony and hope someone can help me. I have an entity called Material and an associated entity called MaterialKeyword, which are basically tags. I am displaying the keywords comma delimited as a string in a text field on a form. I created a data transformer to do that. Pulling the keywords from the database and displaying them is no problem, but I have a problem with the reversTransform function when I want to submit existing or new keywords to the database.
Material class (MaterialKeyword):
/**
* #Assert\Type(type="AppBundle\Entity\MaterialKeyword")
* #Assert\Valid()
* #ORM\ManyToMany(targetEntity="MaterialKeyword", inversedBy="material")
* #ORM\JoinTable(name="materials_keyword_map",
* joinColumns={#ORM\JoinColumn(name="materialID", referencedColumnName="materialID", nullable=false)},
* inverseJoinColumns={#ORM\JoinColumn(name="keywordID", referencedColumnName="id", nullable=false)})
*/
public $materialkeyword;
/**
* Constructor
*/
public function __construct()
{
$this->MaterialKeyword = new ArrayCollection();
}
/**
* Set materialkeyword
*
* #param array $materialkeyword
*
*/
public function setMaterialkeyword(MaterialKeyword $materialkeyword=null)
{
$this->materialkeyword = $materialkeyword;
}
/**
* Get materialkeyword
*
* #Assert\Type("\array")
* #return array
*/
public function getMaterialkeyword()
{
return $this->materialkeyword;
}
Here is my code from the data transformer:
This part is working:
class MaterialKeywordTransformer implements DataTransformerInterface
{
/**
* #var EntityManagerInterface
*/
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* Transforms an object (materialkeyword) to a string.
*
* #param MaterialKeyword|null $materialkeyword
* #return string
*/
public function transform($material)
{
$result = array();
if (null === $material) {
return '';
}
foreach ($material as $materialkeyword) {
$result[] = $materialkeyword->getKeyword();
}
return implode(", ", $result);
}
This part is not working:
/**
* Transforms a string (keyword) to an object (materialkeyword).
*
* #param string $materialkeyword
* #return MaterialKeyword|null
* #throws TransformationFailedException if object (materialkeyword) is not found.
*/
public function reverseTransform($keywords)
{
// no keyword? It's optional, so that's ok
if (!$keywords) {
return;
}
$repository = $this->manager
->getRepository('AppBundle:MaterialKeyword');
$keyword_array = explode(", ", $keywords);
foreach($keyword_array as $keyword){
$materialkeyword = new MaterialKeyword();
$keyword_entry = $repository->findBy(array('keyword' => $keyword));
if(array_key_exists(0, $keyword_entry)){
$keyword_entry_first = $keyword_entry[0];
}else{
$keyword_entry_first = $keyword_entry;
}
if (null === $keyword_entry_first) {
throw new TransformationFailedException(sprintf('There is no "%s" exists',
$keywords
));
}
$materialkeyword->setKeyword($keyword_entry_first);
}
return $materialkeyword;
}
There will be several keywords, so how do I store them. I tried Arrays and ArrayCollections (new ArrayCollection()) without any success.
The error that I am getting currently with the code above:
Catchable Fatal Error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given, called in /.../vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 605 and defined
TL;DR;
Your reverseTransform function should return an array containing zero or n MaterialKeyword.
It should not return a single MaterialKeyword object because the reverse transformation of MaterialKeyord[] --> string is not string --> MaterialKeyword, it is string --> MaterialKeyword[].
Thinking about this, the doctrine ArrayCollection exception you have make sense as it is trying to do new ArrayCollection(/** Single MaterialKeyword object */) instead of new ArrayCollection(/** Array of MaterialKeyword objects */).
From what you're telling I assume that Material and MaterialKeyword are connected by a ManyToMany association, in which case each Material has an array of MaterialKeyword objects associated to it.
Which means, that your Data Transformer should work with arrays as well, but you're only working with single objects.
Specifically, reverseTransform should return an array of MaterialKeyword objects, whereas you're only returning one (the last one handled in the loop.)
Another issue is that your method created new objects every time, even though $repository->findBy(...) would already return a MaterialKeyword instance. Creating a new object would cause that entry to be copied instead of simply used.
So the correct method might look like this:
public function reverseTransform($keywords)
{
// no keyword? It's optional, so that's ok
if (!$keywords) {
return array();
}
$repository = $this->manager
->getRepository('AppBundle:MaterialKeyword');
$keyword_array = explode(", ", $keywords);
$result_list = array(); // This will contain the MaterialKeyword objects
foreach($keyword_array as $keyword){
$keyword_entry = $repository->findOneBy(array('keyword' => $keyword));
if (null === $keyword_entry) {
throw new TransformationFailedException(sprintf('There is no "%s" exists',
$keyword
));
}
$result_list[] = $keyword_entry;
}
return $result_list;
}
#Hanzi put me on the correct track. It has to be an array of MaterialKeywords objects.
Here is my final working code in class MaterialKeywordTransformer:
/**
* Transforms a string (keyword) to an object (materialkeyword).
*
* #param string $materialkeyword
* #return MaterialKeyword|null
* #throws TransformationFailedException if object (materialkeyword) is not found.
*/
public function reverseTransform($keywords)
{
// keyword are optional, so that's ok
if (!$keywords) {
return;
}
$repository = $this->manager
->getRepository('AppBundle:MaterialKeyword');
$repository_m = $this->manager
->getRepository('AppBundle:Material');
$keyword_array = explode(", ", $keywords);
foreach($keyword_array as $keyword){
$materialkeyword = new MaterialKeyword();
$materialkeyword->setKeyword($keyword);
if($this->opt["data"]->getMaterialID() !== null) {
$materialkeyword->setMaterialID($this->opt["data"]->getMaterialID());
} else {
$material = $repository_m->findOne();
$materialID = $material[0]->getMaterialID();
$materialkeyword->setMaterialID($materialID);
}
$materialkeywords[] = $materialkeyword;
if (null === $keywords) {
throw new TransformationFailedException(sprintf('There is no "%s" exists',
$keywords
));
}
}
return $materialkeywords;
}

Doctrine replace associations with their ids

I'm building an API using FOSRestBundle in Symfony2 and I'm trying to remove the entity associations from an entity with no luck, to minimize the JSON data sent in the API response.
public function helperRemoveAssociations($entities) {
$em = $this->getEntityManager();
$data = array();
/** #var WBEntity $entity */
foreach ($entities as $entity) {
$entityAssociations = $entity->getAssociationNames($em); // ex: array('category', 'comments')
foreach ($entityAssociations as $associationName) {
$associationGetter = 'get' . ucfirst($associationName);
$associationSetter = 'set' . ucfirst($associationName);
/** #var WBEntity $associationObject */
$associationObject = $entity->$associationGetter();
$associationId = $associationObject->getEntityId($em); // ex: 51
$entity->$associationSetter($associationId); // I am not allowed to write an integer
}
$data[] = $entity;
}
return $data;
}
How can I accomplish this, replacing the associated entities with their respective ids.

Upload multiple files for each entity

I am using doctrine2 with symfony2.
This is my entity to upload the file.
First, it call the setFile() and put the path to $this->temp,
then,preUpload is called ,upload called.
It is OK for uploading onefile for each entity,however, I would like to upload multiple files for each entity.
How can I handle this ?
Do you have any samples for this purpose?
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
public $path = "nophoto.jpeg";
/**
* #Assert\File(maxSize="6000000")
*/
private $file;
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
// check if we have an old image path
if (is_file($this->getAbsolutePath())) {
// store the old name to delete after the update
$this->temp = $this->getAbsolutePath();
} else {
$this->path = 'initial';
}
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->getFile()) {
$this->path = $this->getId().'.'.$this->getFile()->guessExtension();
}
/**
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->getFile1()) {return;}
if (isset($this->temp)) {
// delete the old image
unlink($this->temp);
// clear the temp image path
$this->temp = null;
}
// you must throw an exception here if the file cannot be moved
// so that the entity is not persisted to the database
// which the UploadedFile move() method does
$this->getFile()->move(
$this->getUploadRootDir(),
$this->getId().'.'.$this->getFile()->guessExtension()
);
$this->setFile(null);
}
public function getAbsolutePath()
{
return null === $this->path
? null
: $this->getUploadRootDir().'/'.$this->getId().'.'.$this->path;
}
public function getFile1()
{
return $this->file;
}
public function getWebPath()
{
return null === $this->path
? null
: $this->getUploadDir().'/'.$this->path;
}
protected function getUploadRootDir()
{
// the absolute directory path where uploaded
// documents should be saved
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
protected function getUploadDir()
{
// get rid of the __DIR__ so it doesn't screw up
// when displaying uploaded doc/image in the view.
return 'uploads/documents';
}
You need new entity which will represent uploaded file with many-to-one (or many-to-many) association to your entity. This is most universal approach.
Alternatively you can store file names in array but that will complicate your validation and forms.

Symfony2 and ParamConverter(s)

Accessing my route /message/new i'm going to show a form for sending a new message to one or more customers. Form model has (among others) a collection of Customer entities:
class MyFormModel
{
/**
* #var ArrayCollection
*/
public $customers;
}
I'd like to implement automatic customers selection using customers GET parameters, like this:
message/new?customers=2,55,543
This is working now by simply splitting on , and do a query for getting customers:
public function newAction(Request $request)
{
$formModel = new MyFormModel();
// GET "customers" parameter
$customersIds = explode($request->get('customers'), ',');
// If something was found in "customers" parameter then get entities
if(!empty($customersIds)) :
$repo = $this->getDoctrine()->getRepository('AcmeHelloBundle:Customer');
$found = $repo->findAllByIdsArray($customersIds);
// Assign found Customer entities
$formModel->customers = $found;
endif;
// Go on showing the form
}
How can i do the same using Symfony 2 converters? Like:
public function newAction(Request $request, $selectedCustomers)
{
}
Answer to my self: there is not such thing to make you life easy. I've coded a quick and dirty (and possibly buggy) solution i'd like to share, waiting for a best one.
EDIT WARNING: this is not going to work with two parameter converters with the same class.
Url example
/mesages/new?customers=2543,3321,445
Annotations:
/**
* #Route("/new")
* #Method("GET|POST")
* #ParamConverter("customers",
* class="Doctrine\Common\Collections\ArrayCollection", options={
* "finder" = "getFindAllWithMobileByUserQueryBuilder",
* "entity" = "Acme\HelloBundle\Entity\Customer",
* "field" = "id",
* "delimiter" = ",",
* }
* )
*/
public function newAction(Request $request, ArrayCollection $customers = null)
{
}
Option delimiter is used to split GET parameter while id is used for adding a WHERE id IN... clause. There are both optional.
Option class is only used as a "signature" to tell that converter should support it. entity has to be a FQCN of a Doctrine entity while finder is a repository method to be invoked and should return a query builder (default one provided).
Converter
class ArrayCollectionConverter implements ParamConverterInterface
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
function apply(Request $request, ConfigurationInterface $configuration)
{
$name = $configuration->getName();
$options = $this->getOptions($configuration);
// Se request attribute to an empty collection (as default)
$request->attributes->set($name, new ArrayCollection());
// If request parameter is missing or empty then return
if(is_null($val = $request->get($name)) || strlen(trim($val)) === 0)
return;
// If splitted values is an empty array then return
if(!($items = preg_split('/\s*'.$options['delimiter'].'\s*/', $val,
0, PREG_SPLIT_NO_EMPTY))) return;
// Get the repository and logged user
$repo = $this->getEntityManager()->getRepository($options['entity']);
$user = $this->getSecurityContext->getToken()->getUser();
if(!$finder = $options['finder']) :
// Create a new default query builder with WHERE user_id clause
$builder = $repo->createQueryBuilder('e');
$builder->andWhere($builder->expr()->eq("e.user", $user->getId()));
else :
// Call finder method on repository
$builder = $repo->$finder($user);
endif;
// Edit the builder and add WHERE IN $items clause
$alias = $builder->getRootAlias() . "." . $options['field'];
$wherein = $builder->expr()->in($alias, $items);
$result = $builder->andwhere($wherein)->getQuery()->getResult();
// Set request attribute and we're done
$request->attributes->set($name, new ArrayCollection($result));
}
public function supports(ConfigurationInterface $configuration)
{
$class = $configuration->getClass();
// Check if class is ArrayCollection from Doctrine
if('Doctrine\Common\Collections\ArrayCollection' !== $class)
return false;
$options = $this->getOptions($configuration);
$manager = $this->getEntityManager();
// Check if $options['entity'] is actually a Dcontrine one
try
{
$manager->getClassMetadata($options['entity']);
return true;
}
catch(\Doctrine\ORM\Mapping\MappingException $e)
{
return false;
}
}
protected function getOptions(ConfigurationInterface $configuration)
{
return array_replace(
array(
'entity' => null,
'finder' => null,
'field' => 'id',
'delimiter' => ','
),
$configuration->getOptions()
);
}
/**
* #return \Doctrine\ORM\EntityManager
*/
protected function getEntityManager()
{
return $this->container->get('doctrine.orm.default_entity_manager');
}
/**
* #return \Symfony\Component\Security\Core\SecurityContext
*/
protected function getSecurityContext()
{
return $this->container->get('security.context');
}
}
Service definition
arraycollection_converter:
class: Acme\HelloBundle\Request\ArrayCollectionConverter
arguments: ['#service_container']
tags:
- { name: request.param_converter}
It's late, but according to latest documentation about #ParamConverter, you can achieve it follow way:
* #ParamConverter("users", class="AcmeBlogBundle:User", options={
* "repository_method" = "findUsersByIds"
* })
you just need make sure that repository method can handle comma (,) separated values

Resources