Query on Doctrine class table inheritance with required fields - symfony

My domain has a parent IncidenceMessage class and several child classes (i.e. IncidenceMessageText).
I have the following class table inheritance configuration:
Domain\Model\IncidenceMessage\IncidenceMessage:
type: entity
repositoryClass: Infrastructure\Domain\Model\IncidenceMessage\DoctrineIncidenceMessageRepository
table: incidence_messages
inheritanceType: JOINED
discriminatorColumn:
name: type
type: string
length: 30
discriminatorMap:
text: IncidenceMessageText
image: IncidenceMessageImage
audio: IncidenceMessageAudio
video: IncidenceMessageVideo
fields:
...
I can create any IncidenceMessage entity correctly.
Having only a IncidenceMessageText in database, when I try to fetch incidence messages I get the following error:
TypeError: Argument 1 passed to Domain\Model\File\FileId::__construct() must be of the type string, null given
(FileId is a value object that represents the id of a File entity)
IncidenceMessageImage has a File field that is a foreign key and it is required.
It makes no sense to me that Doctrine fetches File when IncidenceMessageText doesn't have that field.
While debugging, I discovered that doctrine does a SELECT with LEFT JOINs with every single IncidenceMessage table and this calls my FileTypeId::convertToPHPValue method:
class FileIdType extends Type
{
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return new FileId($value);
}
}
AFAIK, the problem here is that child classes have required fields but that shouldn't be a stopper, right?

I found a possible workaround. On my custom DBAL Type FileIdType, I checked if the value was null before instantiating FileId:
use Doctrine\DBAL\Types\Type;
class FileIdType extends Type
{
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return $value ? new FileId($value) : null;
}
}

Related

How to override the PUT operation update process in the Api Platform

I'm trying to override the PUT operation to perform my actions under certain conditions. That is, if the sent object is different from the original object (from the database), then I need to create a new object and return it without changing the original object.
Now when I execute the query I get a new object, as expected, but the problem is that the original object also changes
Entity
#[ApiResource(
operations: [
new Get(),
new GetCollection(),
new Post(controller: CreateAction::class),
new Put(processor: EntityStateProcessor::class),
],
paginationEnabled: false
)]
class Entity
EntityStateProcessor
final class PageStateProcessor implements ProcessorInterface
{
private ProcessorInterface $decorated;
private EntityCompare $entityCompare;
public function __construct(ProcessorInterface $decorated, EntityCompare $entityCompare)
{
$this->decorated = $decorated;
$this->entityCompare = $entityCompare;
}
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
if (($this->entityCompare)($data)) { // checking for object changes
$new_entity = clone $data; // (without id)
// do something with new entity
return $this->decorated->process($new_entity, $operation, $uriVariables, $context);
}
return $data;
}
}
I don't understand why this happens, so I return a clone of the original object to the process. It would be great if someone could tell me what my mistake is.
I also tried the following before returning the process
$this->entityManager->refresh($data); - Here I assumed that the original instance of the object will be updated with data from the database and the object will not be updated with data from the query
$this->entityManager->getUnitOfWork()->detach($data); - Here I assumed that the object would cease to be manageable and would not be updated
But in both cases the state of the original $data changes.
I'm using ApiPlatform 3.0.2
The error is that the main entity is related to an additional entity, so it's not enough to detach the main entity from UnitOfWork. So use the Doctrine\ORM\UnitOfWork->clear(YourEntity::class) method to detach all instances of the entity, and you do the same for relationships.
Once the entity is detach, cloning the entity becomes pointless because the previous entity instance isn't managed by the Doctrine ORM, so my code rearranges itself like this:
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
if (($this->entityCompare)($data)) { // checking for object changes
$this->getEntityManager()->getUnitOfWork()->clear(Entity::class);
$this->getEntityManager()->getUnitOfWork()->clear(EelatedEntity::class);
// do something with new entity
return $this->decorated->process($data, $operation, $uriVariables, $context);
}
return $data;
}

Symfony fixtures return "Invalid parameter number: number of bound variables does not match number of tokens"

I have such a Doctrine fixture:
<?php
declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\Tenant;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Id\IdentityGenerator;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\Persistence\ObjectManager;
final class TenantFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
$tenant = new Tenant();
$tenant->setDomain('1.example.com');
$tenant->setName('Tenant 1');
$manager->persist($tenant);
$manager->flush();
$metadata = $manager->getClassMetadata(Tenant::class);
$metadata->setIdGenerator(new AssignedGenerator());
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE);
$tenant = new Tenant();
$tenant->setId(100);
$tenant->setDomain('2.example.com');
$tenant->setName('Tenant 2');
$manager->persist($tenant);
$manager->flush();
$metadata->setIdGenerator(new IdentityGenerator());
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
}
}
When I try to load it via bin/console doctrine:fixtures:load --env=test, I receive such an error:
An exception occurred while executing 'INSERT INTO tenant (name, domain) VALUES (?, ?)' with params [100, "Tenant 2", "2.example.com"]:
SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens
How to save both entities correctly?
Note that the first one has an auto generated id, and the second one has an assigned id. This example is simplified.

API Platform + alice UserDataPersister not working with fixtures

I have the following UserDataPersister (taken straight from the tutorial) configured:
Information for Service "App\DataPersister\UserDataPersister"
=============================================================
Service ID App\DataPersister\UserDataPersister
Class App\DataPersister\UserDataPersister
Tags api_platform.data_persister (priority: -1000)
Public no
Shared yes
Abstract no
Autowired yes
Autoconfigured yes
and the following User fixture:
App\Entity\User:
user_{1..10}:
email: "usermail_<current()>\\#email.org"
plainPassword: "plainPassword_<current()>"
__calls:
- initUuid: []
But I get errors when loading this fixture:
An exception occurred while executing 'INSERT INTO "user" (id, uuid, roles, password, email) VALUES (?, ?, ?, ?, ?)' with params [281, "16ac40d3-53af-45dc-853f-e26f188d
1818", "[]", null, "usermail1#email.org"]:
SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "password" of relation "user" violates not-null constraint
DETAIL: Failing row contains (281, 16ac40d3-53af-45dc-853f-e26f188d1818, [], null, usermail1#email.org).
My implementation of UserDataPersister is identical with this.
Quote from Article at the end
If we stopped now... yay! We haven't... really... done anything: we
added this new plainPassword property... but nothing is using it! So,
the request would ultimately explode in the database because our
$password field will be null.
Next, we need to hook into the request-handling process: we need to
run some code after deserialization but before persisting. We'll do
that with a data persister.
Since unit test would POST the request, the data persistor is called by api-platform and it will pick up encoding logic by event. In case of fixtures, direct doctrine batch insert is done, this will bypass all persistence logic and would result in null password.
There is a way to solve this as mentioned by #rishta Use Processor to implement hash to your data fixtures as referenced in Documentation
<?php
namespace App\DataFixtures\Processor;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Fidry\AliceDataFixtures\ProcessorInterface;
use App\Entity\User;
final class UserProcessor implements ProcessorInterface
{
private $userPasswordEncoder;
public function __construct(EntityManagerInterface $entityManager, UserPasswordEncoderInterface $userPasswordEncoder) {
$this->userPasswordEncoder = $userPasswordEncoder;
}
/**
* #inheritdoc
*/
public function preProcess(string $fixtureId, $object): void {
if (false === $object instanceof User) {
return;
}
$object = $this->userPasswordEncoder(
$object,
$object->getPlainPassword()
);
}
/**
* #inheritdoc
*/
public function postProcess(string $fixtureId, $object): void
{
// do nothing
}
}
Register service :
# app/config/services.yml
services:
_defaults:
autoconfigure: true
App\DataFixtures\Processor\UserProcessor: ~
#add tag in case autoconfigure is disabled, no need for auto config
#tags: [ { name: fidry_alice_data_fixtures.processor } ]
One of the better ways to do input masking in API Platform is to use DTO Pattern as oppose to suggested by article, in which you are allowed to :
Create separate input & output data objects
Transform Underlying date to and from the objects
Choose Different IO objects for each operation whenever needed
More on DTO in documentation

How to denormalize an array recursively in Symfony 5?

I am currently trying to denormalize an array, which came out of an API as a JSON response and was JSON decoded.
The problem is, that I want it to be denormalized into a class and one of the properties is another class.
It feels like it should be possible to get such an easy job done with the Symfony denormalizer, but I always get the following exception:
Failed to denormalize attribute "inner_property" value for class "App\Model\Api\Outer": Expected argument of type "App\Model\Api\Inner", "array" given at property path "inner_property".
My denormalizing code looks like that:
$this->denormalizer->denormalize($jsonOuter, Outer::class);
The denormalizer is injected in the constructor:
public function __construct(DenormalizerInterface $denormalizer) {
The array I try to denormalize:
array (
'inner_property' =>
array (
'property' => '12345',
),
)
Finally the both classes I try to denormalize to:
class Outer
{
/** #var InnerProperty */
private $innerProperty;
public function getInnerProperty(): InnerProperty
{
return $this->innerProperty;
}
public function setInnerProperty(InnerProperty $innerProperty): void
{
$this->innerProperty = $innerProperty;
}
}
class InnerProperty
{
private $property;
public function getProperty(): string
{
return $this->property;
}
public function setProperty(string $property): void
{
$this->property = $property;
}
}
After hours of searching I finally found the reason. The problem was the combination of the "inner_property" snake case and $innerProperty or getInnerProperty camel case. In Symfony 5 the camel case to snake case converter is not enabled by default.
So I had to do this by adding this config in the config/packages/framework.yaml:
framework:
serializer:
name_converter: 'serializer.name_converter.camel_case_to_snake_case'
Here is the reference to the Symfony documentation: https://symfony.com/doc/current/serializer.html#enabling-a-name-converter
Alternatively I could have also add a SerializedName annotation to the property in the Outer class:
https://symfony.com/doc/current/components/serializer.html#configure-name-conversion-using-metadata
PS: My question was not asked properly, because I didn't changed the property and class names properly. So I fixed that in the question for future visitors.

Cannot Update Entity Using EF 6 - ObjectStateManager Error

I'm trying to update an entity using Entity Framework version 6.
I'm selecting the entity from the database like so...
public T Find<T>(object id) where T : class
{
return this._dbContext.Set<T>().Find(id);
}
And updating the entity like so..
public T Update<T>(T entity) where T : class
{
// get the primary key of the entity
object id = this.GetPrimaryKeyValue(entity);
// get the original entry
T original = this._dbContext.Set<T>().Find(id);
if (original != null)
{
// do some automatic stuff here (taken out for example)
// overwrite original property values with new values
this._dbContext.Entry(original).CurrentValues.SetValues(entity);
this._dbContext.Entry(original).State = EntityState.Modified;
// commit changes to database
this.Save();
// return entity with new property values
return entity;
}
return default(T);
}
The GetPrimaryKeyValue function is as so...
private object GetPrimaryKeyValue<T>(T entity) where T : class
{
var objectStateEntry = ((IObjectContextAdapter)this._dbContext).ObjectContext
.ObjectStateManager
.GetObjectStateEntry(entity);
return objectStateEntry.EntityKey.EntityKeyValues[0].Value;
}
Just for clarity. I'm selecting the original entry out as I need to perform some concurrency logic (that Ive taken out). I'm not posting that data with the entity and need to select it manually out of the DB again to perform the checks.
I know the GetPrimaryKeyValue function is not ideal if there's more than one primary key on the entity. I just want it to work for now.
When updating, entity framework coughs up the error below when trying to execute the GetPrimaryKeyValue function.
The ObjectStateManager does not contain an ObjectStateEntry with a reference to an object of type 'NAME_OF_ENTITY_IT_CANNOT_FIND'
I've written many repositories before and I've never had this issue, I cannot seem to find why its not working (hence the post).
Any help would be much appreciated.
Thanks guys!
Steve
It seems like you are having issues getting the PK from the entity being passed in. Instead of trying to go through EF to get this data you could either use their Key attribute or create your own and just use reflection to collect what the key names are. This will also allow you to retrieve multiple keys if it is needed. Below is an example I created inside of LinqPad, you should be able to set it to "Program" mode and paste this in and see it work. Hack the code up and use what you may. I implemented an IEntity but it is not required, and you can change the attribute to anything really.
Here are the results:
Keys found:
CustomIdentifier
LookASecondKey
Here is the code:
// this is just a usage demo
void Main()
{
// create your object from wherever
var car = new Car(){ CustomIdentifier= 1, LookASecondKey="SecretKey", Doors=4, Make="Nissan", Model="Altima" };
// pass the object in
var keys = GetPrimaryKeys<Car>(car);
// you have the list of keys now so work with them however
Console.WriteLine("Keys found: ");
foreach(var k in keys)
Console.WriteLine(k);
}
// you probably want to use this method, add whatever custom logic or checking you want, maybe put
private IEnumerable<string> GetPrimaryKeys<T>(T entity) where T : class, IEntity
{
// place to store keys
var keys = new List<string>();
// loop through each propery on the entity
foreach(var prop in typeof(T).GetProperties())
{
// check for the custom attribute you created, replace "EntityKey" with your own
if(prop.CustomAttributes.Any(p => p.AttributeType.Equals(typeof(EntityKey))))
keys.Add(prop.Name);
}
// check for key and throw if not found (up to you)
if(!keys.Any())
throw new Exception("No EntityKey attribute was found, please make sure the entity includes this attribute on at least on property.");
// return all the keys
return keys;
}
// example of the custom attribute you could use
[AttributeUsage(AttributeTargets.Property)]
public class EntityKey : Attribute
{
}
// this interface is not NEEDED but I like to restrict dal to interface
public interface IEntity { }
// example of your model
public class Car : IEntity
{
[EntityKey] // add the attribure to property
public int CustomIdentifier {get;set;}
[EntityKey] // i am demonstrating multiple keys but you can have just one
public string LookASecondKey {get;set;}
public int Doors {get;set;}
public string Make {get;set;}
public string Model {get;set;}
}

Resources