Recently I've started using the Symfony Validator component in my importer script to validate if an entity has all required fields set and that there are no "unique constraint violations". Invalid entries are skipped and logged.
I'm not sure but this github issue might describe the same problem: https://github.com/doctrine/orm/issues/7277
the validator component is at version v4.2.5
and the doctrine library is at version v2.6.3
Anyone had a problem like this before? How do I solve this?
error: {
message: "Argument 2 passed to Doctrine\ORM\Cache\EntityCacheKey::__construct() must be of the type array, null given, called in /var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/Cache/DefaultQueryCache.php on line 353",
trace: [
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/Cache/EntityCacheKey.php:49",
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/Cache/DefaultQueryCache.php:353",
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/Cache/DefaultQueryCache.php:305",
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php:426",
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/EntityRepository.php:181",
"/var/www/myproject/vendor/symfony/doctrine-bridge/Validator/Constraints/UniqueEntityValidator.php:139",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:809",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:525",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:330",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:141",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveValidator.php:100",
"/var/www/myproject/src/Service/Api/Import/StoreImporter.php:251",
"/var/www/myproject/src/Command/Api/Import/StoreImporterCommand.php:92",
"/var/www/myproject/vendor/symfony/console/Command/Command.php:255",
"/var/www/myproject/vendor/symfony/console/Application.php:926",
"/var/www/myproject/vendor/symfony/framework-bundle/Console/Application.php:89",
"/var/www/myproject/vendor/symfony/console/Application.php:269",
"/var/www/myproject/vendor/symfony/framework-bundle/Console/Application.php:75",
"/var/www/myproject/vendor/symfony/console/Application.php:145",
"/var/www/myproject/bin/console:39"
]
},
In another Importer an issue arises which is also related to second level cache. The stack trace shows one difference, it uses the liip/functional-test-bundle DataCollectingValidator because this is when I ran the command in a 'dev' environment.
error: {
message: "Notice: Undefined index: 0000000051f7928f000000005709f6ca",
trace: [
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:2995",
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/Cache/DefaultQueryCache.php:352",
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/Cache/DefaultQueryCache.php:305",
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php:426",
"/var/www/myproject/vendor/doctrine/orm/lib/Doctrine/ORM/EntityRepository.php:181",
"/var/www/myproject/vendor/symfony/doctrine-bridge/Validator/Constraints/UniqueEntityValidator.php:139",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:809",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:525",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:330",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveContextualValidator.php:141",
"/var/www/myproject/vendor/symfony/validator/Validator/RecursiveValidator.php:100",
"/var/www/myproject/vendor/symfony/validator/Validator/TraceableValidator.php:66",
"/var/www/myproject/vendor/liip/functional-test-bundle/src/Validator/DataCollectingValidator.php:66",
"/var/www/myproject/src/Service/Api/Import/DeviceImporter.php:272",
"/var/www/myproject/src/Command/Api/Import/DeviceImporterCommand.php:92",
"/var/www/myproject/vendor/symfony/console/Command/Command.php:255",
"/var/www/myproject/vendor/symfony/console/Application.php:926",
"/var/www/myproject/vendor/symfony/framework-bundle/Console/Application.php:89",
"/var/www/myproject/vendor/symfony/console/Application.php:269",
"/var/www/myproject/vendor/symfony/framework-bundle/Console/Application.php:75",
"/var/www/myproject/vendor/symfony/console/Application.php:145",
"/var/www/myproject/bin/console:39"
]
}
edit: added the configuration of Unique Entity Constraints:
/**
* Device Entity
*
* @ORM\Cache(region = "rarely_changing", usage = "NONSTRICT_READ_WRITE")
* @ORM\Entity(repositoryClass = "App\Repository\DeviceRepository")
* @ORM\Table(name = "s4_device", indexes = {
* @ORM\Index(name = "device_model_search_index", columns = {"device_model"}),
* @ORM\Index(name = "device_slug_search_index", columns = {"device_slug"}),
* }, uniqueConstraints = {
* @ORM\UniqueConstraint(name = "UNIQUE_DEVICE_MODEL_AND_BRAND", columns = {"device_model", "brand_id"})
* })
* @UniqueEntity(fields = {"model", "brand"}, errorPath = "model", message = "device-model-is-not-unique")
*/
class Device extends AbstractEntity
{
/* ... */
/**
* @var DeviceReference
*
* @ORM\Cache(region = "rarely_changing", usage = "NONSTRICT_READ_WRITE")
* @ORM\OneToOne(targetEntity = "App\Entity\Model\Reference\DeviceReference", mappedBy = "device", cascade = {"persist", "remove"})
*/
protected $reference;
/* ... */
}
/**
* Store Entity
*
* @ORM\Cache(region = "rarely_changing", usage = "NONSTRICT_READ_WRITE")
* @ORM\Entity(repositoryClass = "App\Repository\StoreRepository")
* @ORM\Table(name = "s4_store", indexes = {
* @ORM\Index(name = "store_name_search_index", columns = {"store_name"}),
* @ORM\Index(name = "store_slug_search_index", columns = {"store_slug"})
* }, uniqueConstraints = {
* @ORM\UniqueConstraint(name = "UNIQUE_STORE_NAME", columns = {"store_name"})
* })
* @UniqueEntity(fields = {"name"}, message = "store-name-is-not-unique")
* @UniqueEntity(fields = {"uri"}, message = "store-uri-is-not-unique")
*/
class Store extends AbstractEntity
{
/* ... */
/**
* @var StoreReference|null
*
* @ORM\Cache(region = "rarely_changing", usage = "NONSTRICT_READ_WRITE")
* @ORM\OneToOne(targetEntity = "App\Entity\Model\Reference\StoreReference", mappedBy = "store", cascade = {"persist", "remove"})
*/
protected $reference;
/* ... */
}
Store Importer (example where unrelated code is ommitted)
namespace App\Service\Api\Import;
/* ... */
use Symfony\Component\Validator\Validator\ValidatorInterface;
class StoreImporter
{
/**
* @var StoreManagerInterface
*/
private $_storeManager;
/**
* @var ValidatorInterface
*/
private $_validator;
/**
* @param StoreManagerInterface $storeManager
* @param ValidatorInterface $validator
*/
public function __construct(StoreManagerInterface $storeManager, ValidatorInterface $validator)
{
$this->_storeManager = $storeManager;
$this->_validator = $validator;
}
public function import()
{
/* ... */
foreach ($storeHashes as $md5hash => $storeDetails) {
/* @var Store $store */
$store = $this->_storeManager->findOrCreateByReference(
$storeDetails["company_id"], // cs-cart ID
$storeDetails["company"] // store name
);
/* ... */
$validationErrors = $this->_validator->validate($store);
if (count($validationErrors) > 0) {
$validationContext = [
"errors" => (string) $validationErrors,
"details" => $storeDetails,
];
$this->logError("Skipping " . (string) $store . " - Store has validation errors", $validationContext);
continue; // skip saving store details when there are validation errors
}
/* ... */
$this->_storeManager->saveOne($store, false);
}
/* ... */
$this->_storeManager->flush();
$this->_storeManager->clear();
/* ... */
}
/* ... */
}
Edit #2: this is what happens at line 139 of the Unique Entity Validator.
138
139 $result = $repository->{$constraint->repositoryMethod}($criteria);
140
the dumped variables ($constraint and $criteria)
"constraint" => Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity {#2 462
+message: "device-model-is-not-unique"
+service: "doctrine.orm.validator.unique"
+em: null
+entityClass: null
+repositoryMethod: "findBy"
+fields: array:2 [
0 => "model"
1 => "brand"
]
+errorPath: "model"
+ignoreNull: true
+payload: null
+"groups": array:2 [
0 => "Default"
1 => "Device"
]
}
"criteria" => array:2 [
"model" => "Xperia XZ"
"brand" => App\Entity\Model\DeviceBrand {#2183
#type: "device_brand"
#devices: Doctrine\ORM\PersistentCollection {#2184
-snapshot: []
-owner: App\Entity\Model\DeviceBrand {#2183}
-association: array:16 [ …16]
-em: Doctrine\ORM\EntityManager {#561 …11}
-backRefFieldName: "brand"
-typeClass: Doctrine\ORM\Mapping\ClassMetadata {#128 …}
-isDirty: false
#collection: Doctrine\Common\Collections\ArrayCollection {#2185
-elements: []
}
#initialized: false
}
#id: 14
#name: "Sony"
#slug: "sony"
#createdAt: DateTime @1560754604 {#2182
date: 2019-06-17 08:56:44.0 Europe/Berlin (+02:00)
}
#createdBy: null
#deleted: false
#deletedAt: null
#deletedBy: null
#updatedAt: null
#updatedBy: null
}
]
Edit #3:
I've found some bit of odd behaviour by trying to debug the UnitOfWork class in Doctrine ORM library. Apparently a object hash can't be found in the entityIdentifiers array. The type of entity that can't be found is a OneToOne relation to the entity that I'm updating.
UnitOfWork (with modification)
/**
* Gets the identifier of an entity.
* The returned value is always an array of identifier values. If the entity
* has a composite identifier then the identifier values are in the same
* order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
*
* @param object $entity
*
* @return array The identifier values.
*/
public function getEntityIdentifier($entity)
{
if (!array_key_exists(spl_object_hash($entity), $this->entityIdentifiers)) {
dump([
"entity-class" => get_class($entity),
"object-hash" => spl_object_hash($entity),
"identifiers" => $this->entityIdentifiers,
]);
exit();
}
return $this->entityIdentifiers[spl_object_hash($entity)];
}
dump result:
array:3 [
"entity-class" => "App\Entity\Model\Reference\StoreReference"
"object-hash" => "000000002f4c81ef00000000678832e6"
"identifiers" => array:6 [
"000000002f4c805b00000000678832e6" => array:1 [
"id" => 1
]
"000000002f4c805700000000678832e6" => array:1 [
"id" => 2
]
"000000002f4c813600000000678832e6" => array:1 [
"id" => 6
]
"000000002f4c816100000000678832e6" => array:1 [
"id" => 2
]
"000000002f4c819f00000000678832e6" => array:1 [
"id" => 1
]
"000000002f4c81e200000000678832e6" => array:1 [
"id" => 1
]
]
]
Store Reference Entity
namespace App\Entity\Model\Reference;
use App\Entity\Model\Store;
use Doctrine\ORM\Mapping as ORM;
use App\Entity\Model\Reference\AbstractReferenceEntity;
/**
* @ORM\Cache(region = "rarely_changing", usage = "NONSTRICT_READ_WRITE")
* @ORM\Entity()
* @ORM\Table(name = "s4_store_reference", indexes = {
* @ORM\Index(name = "reference_external_id_search_index", columns = {"external_id"})
* })
*/
class StoreReference extends AbstractReferenceEntity
{
/**
* @return string
*/
public function __toString()
{
return "[" . $this->id . "] StoreReference";
}
/**
* ID
*
* @var integer
*
* @ORM\Id
* @ORM\Column(name = "store_reference_id", type = "integer", unique = true)
* @ORM\GeneratedValue(strategy = "AUTO")
*/
protected $id;
/**
* @var Store
*
* @ORM\Cache(region = "rarely_changing", usage = "NONSTRICT_READ_WRITE")
* @ORM\OneToOne(targetEntity = "App\Entity\Model\Store", inversedBy = "reference", cascade = { "persist", "remove" })
* @ORM\JoinColumn(name = "store_id", referencedColumnName = "store_id", nullable = false)
*/
protected $store;
/* ... additional methods ... */
}
I'm also trying to reproduce this problem in this github repository
UniqueEntityValidator
to Doctrine and where something goes wrong. Without knowing how your actual code looks like it's rather impossible to help besides that. – xabbuh$constraints
on line 139 of theUniqueEntityValidator
? – xabbuhbrand
attribute is not a scalar value but an associated object. It may be worth to open a bug report adding a small example application that allows to reproduce this. – xabbuh