How to load 2 tables into one object in Doctrine2? - doctrine-orm

For example, we have an external DB of Countries and Cities. We need to be able to read that external DB with the following conditions:
We can't alter or modify in any way the World DB. We can't add FK for example.
When using the external DB, we want to keep an internal reference, for example for our entity "User" we want to keep a reference such as User->city
We want to have an internal entity CustomCities where users can create their own cities.
What would be the best approach to do this?
We have tried several options but all of them break in one way or another. One recommendation was to use a #Table with an external reference readOnly but that didn't work.
The closest solution we have found for this is to use an in-between class that represents a City object, but doesn't really hold data, and then via native queries, we populate that fake object. Then using internal logic we determine if the requested item such as User->getCity() came from the City DB or came from the CityCustomDB...
Any ideas on how to approach this?

I've taken a guess at the possible schema, have you tried using Class Table Inheritance so that the country essentially becomes your interface.
interface CountryInterface
{
public function getName();
}
So your entities might look like this
/**
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="type", type="string")
* #DiscriminatorMap({
* "internal" = "InternalCountry"
* ,"external" = "ExternalCountryAlias"
* })
*/
abstract class AbstractCountry implements CountryInterface
{
protected $id;
}
class InternalCountry extends AbstractCountry
{
protected $name;
public function getName()
{
return $this->name;
}
}
The ExternalCountryAlias works like a proxy to ExternalCountry, but I named it Alias so not to confuse it with Data Mapper Proxies.
class ExternalCountryAlias extends AbstractCountry
{
/**
* #OneToOne(targetEntity="ExternalCountry")
* #JoinColumn(name="external_country_id"
* ,referencedColumnName="id")
*/
protected $externalCountry;
public function getName()
{
return $this->externalCountry->getName();
}
}
ExternalCountry doesn't have to extend from the base class.
class ExternalCountry
{
protected $name;
public function getName()
{
return $this->name;
}
}
So when you get a country you are referencing the base class. So lets say country.id = 1 is and internal country and country.id = 2 is an external country.
// returns an instance of InternalCountry
$entityManager->find('AbstractCountry', 1);
// returns an instance of ExternalCountryAlias
$entityManager->find('AbstractCountry', 2);
And because they both implement CountryInterface you don't have to worry where they came from, you still access the name by calling getName();

Related

Doctrine 2 custom entity loading and persisting

is it possible to implement a custom hydration and persistence in Doctrine 2 on a per entity basis?
Doctrine 2 has some major limitations regarding value objects (e.g. collections and ids). I wonder if it would be possible to use custom mechanisms (or implementations) for the mapping from object properties to the database (loading and persistence).
I know there are some possibilities to "solve" this problem but I like none of them:
Fake entities require proper handling in the entity which leaks the persistence layer into the domain objects
real entities require a lot more work in persistence (more repositories and more complex handling)
Embaddables have the mentioned limitations
Custom DBAL types with serialization makes querying for certain values impossible or at least extremely slow
I know there are the lifecycle events in doctrine which may be usable. I could't find out if the postLoad event carries an already constructed entity object (with all the VOs)? Becuase in that case it would be useless to me.
best regards,
spigandromeda
Yes, you can register new hydrators in your config/packages/doctrine.yaml like this:
doctrine:
dbal: ...
orm:
hydrators:
CustomEntityHydrator: 'App\ORM\Hydrator\CustomEntityHydrator'
...
mapping: ...
...
You can then use it in your queries like this:
public function findCustomEntities(): array
{
return $this->createQueryBuilder('c')
...your query logic...
->getResult('CustomEntityHydrator');
}
Note, that you can only specify which hydrator you want to use for the root entity. If you fetch associated entities you might end up with a more complicated setup that is hard to debug.
Instead you could consider dealing with value objects (VOs) only in the interface of your entity. In other words, the fields are scalar values, but your method arguments and return values are VOs.
Here is an example with an entity that has a id of type Uuid, a location (some numeric identifier), status (e.g. ternary true/false/null). These are only there to showcase how to deal with different type of value objects:
/**
* #ORM\Entity()
*/
class CustomEntity
{
/**
* #ORM\Id()
* #ORM\Column(type="string", length=64)
*/
private string $id;
/**
* #ORM\Column(type="int")
*/
private int $location;
/**
* #ORM\Column(type="bool, nullable=true)
*/
private bool $status;
private function __construct(Uuid $id, Location $location, Status $status)
{
$this->id = (string) $id;
$this->location = $location->getValue();
$this->status = $status->get();
}
public static function new(Location $location, Status $status): self
{
return new self(Uuid::v4(), $location, $status);
}
public function getId(): Uuid
{
return Uuid::fromString($this->id);
}
public function getLocation(): Location
{
return new Location($this->location);
}
public function activate(): void
{
$this->status = true;
}
public function deactivate(): void
{
$this->status = false;
}
public function isActive(): bool
{
$this->status === true;
}
public function isInactive(): bool
{
$this->status === false;
}
public function isUninitialized(): bool
{
$this->status === null;
}
public function getStatus(): Status
{
if ($this->status === null) {
return new NullStatus();
}
if ($this->status === true) {
return new ActiveStatus();
}
return new InactiveStatus();
}
}
As you can see, you could replace new() with a public constructor. It would work similar with setters. I sometimes even use (private) setters for this in the constructor. In case of the status you don't even need setters if you instead use multiple methods that set the value internally. Similarly you might want to return scalar values instead of a VO in some cases (or the other way around as shown with the status getter and issers).
The point is, your entity looks from the outside as if it would use your VOs, but internally it already switches to a representation that works better with Doctrine ORM. You could even mix this with using VOs and custom types, e.g. for the UUID. You just have to be careful, when your VO needs more info for being constructed than you want to store in the database, e.g. if the numeric location in our example would also use a locale during creation, then we would need to store this (which makes sense as it seems to be related to the numeric id) or we have to hardcode it in the entity or add an abstraction above, that has access to the locale, in which case your entity would likely not return a Location or at least not a LocalizedLocation.
You might also want to consider not having a VO for each and every property in your entity. While it definitely can be helpful, e.g. to wrap an Email into a custom VO to ensure validity instead of just type hinting for string, it might be less useful for something as generic as a (user's) name, which should be very lenient with which strings it accepts as there are a wide variety of names. Using the approach above you can easily introduce a VO later, by adding a new getter for the VO, changing new() or any other method that mutates your property and then not having to change anything in the data model below (unless there is a more drastic change to how the value is represented).

Doctrine2 - How to define table prefix for orm mapping [duplicate]

Like in question topic, how can I setup default table prefix in symfony2?
The best if it can be set by default for all entities, but with option to override for individual ones.
Having just figured this out myself, I'd like to shed some light on exactly how to accomplish this.
Symfony 2 & Doctrine 2.1
Note: I use YML for config, so that's what I'll be showing.
Instructions
Open up your bundle's Resources/config/services.yml
Define a table prefix parameter:
Be sure to change mybundle and myprefix_
parameters:
mybundle.db.table_prefix: myprefix_
Add a new service:
services:
mybundle.tblprefix_subscriber:
class: MyBundle\Subscriber\TablePrefixSubscriber
arguments: [%mybundle.db.table_prefix%]
tags:
- { name: doctrine.event_subscriber }
Create MyBundle\Subscriber\TablePrefixSubscriber.php
<?php
namespace MyBundle\Subscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
class TablePrefixSubscriber implements \Doctrine\Common\EventSubscriber
{
protected $prefix = '';
public function __construct($prefix)
{
$this->prefix = (string) $prefix;
}
public function getSubscribedEvents()
{
return array('loadClassMetadata');
}
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
{
$classMetadata = $args->getClassMetadata();
if ($classMetadata->isInheritanceTypeSingleTable() && !$classMetadata->isRootEntity()) {
// if we are in an inheritance hierarchy, only apply this once
return;
}
$classMetadata->setTableName($this->prefix . $classMetadata->getTableName());
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
if ($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY
&& array_key_exists('name', $classMetadata->associationMappings[$fieldName]['joinTable']) ) { // Check if "joinTable" exists, it can be null if this field is the reverse side of a ManyToMany relationship
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
}
}
}
}
Optional step for postgres users: do something similary for sequences
Enjoy
Alternate answer
This is an update taking into account the newer features available in Doctrine2.
Doctrine2 naming strategy
Doctrine2 uses NamingStrategy classes which implement the conversion from a class name to a table name or from a property name to a column name.
The DefaultNamingStrategy just finds the "short class name" (without its namespace) in order to deduce the table name.
The UnderscoreNamingStrategy does the same thing but it also lowercases and "underscorifies" the "short class name".
Your CustomNamingStrategy class could extend either one of the above (as you see fit) and override the classToTableName and joinTableName methods to allow you to specify how the table name should be constructed (with the use of a prefix).
For example my CustomNamingStrategy class extends the UnderscoreNamingStrategy and finds the bundle name based on the namespacing conventions and uses that as a prefix for all tables.
Symfony2 naming strategy
Using the above in Symfony2 requires declaring your CustomNamingStragery class as a service and then referencing it in your config:
doctrine:
# ...
orm:
# ...
#naming_strategy: doctrine.orm.naming_strategy.underscore
naming_strategy: my_bundle.naming_strategy.prefixed_naming_strategy
Pros and cons
Pros:
running one piece of code to do one single task -- your naming strategy class is called directly and its output is used;
clarity of structure -- you're not using events to run code which alter things that have already been built by other code;
better access to all aspects of the naming conventions;
Cons:
zero access to mapping metadata -- you only have the context that was given to you as parameters (this can also be a good thing because it forces convention rather than exception);
needs doctrine 2.3 (not that much of a con now, it might have been in 2011 when this question was asked :-));
Simshaun's answer works fine, but has a problem when you have a single_table inheritance, with associations on the child entity. The first if-statement returns when the entity is not the rootEntity, while this entity might still have associations that have to be prefixed.
I fixed this by adjusting the subscriber to the following:
<?php
namespace MyBundle\Subscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
class TablePrefixSubscriber implements EventSubscriber
{
protected $prefix = '';
/**
* Constructor
*
* #param string $prefix
*/
public function __construct($prefix)
{
$this->prefix = (string) $prefix;
}
/**
* Get subscribed events
*
* #return array
*/
public function getSubscribedEvents()
{
return array('loadClassMetadata');
}
/**
* Load class meta data event
*
* #param LoadClassMetadataEventArgs $args
*
* #return void
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
{
$classMetadata = $args->getClassMetadata();
// Only add the prefixes to our own entities.
if (FALSE !== strpos($classMetadata->namespace, 'Some\Namespace\Part')) {
// Do not re-apply the prefix when the table is already prefixed
if (false === strpos($classMetadata->getTableName(), $this->prefix)) {
$tableName = $this->prefix . $classMetadata->getTableName();
$classMetadata->setPrimaryTable(['name' => $tableName]);
}
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
if ($mapping['type'] == ClassMetadataInfo::MANY_TO_MANY && $mapping['isOwningSide'] == true) {
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
// Do not re-apply the prefix when the association is already prefixed
if (false !== strpos($mappedTableName, $this->prefix)) {
continue;
}
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
}
}
}
}
}
This has a drawback though;
A not wisely chosen prefix might cause conflicts when it's actually already part of a table name.
E.g. using prefix 'co' when theres a table called 'content' will result in a non-prefixed table, so using an underscore like 'co_' will reduce this risk.
Also, you can use this bundle for the new version of Symfony (4) - DoctrinePrefixBundle
I don't when to implement a solution that involved catching event (performance concern), so I have tried the Alternate Solution but it doesn't work for me.
I was adding the JMSPaymentCoreBundle and wanted to add a prefix on the payment tables.
In this bundle, the definition of the tables are in the Resources\config\doctrine directory (xml format).
I have finally found this solution:
1) copy doctrine directory containing the definitions on the table and paste it in my main bundle
2) modify the name of the tables in the definitions to add your prefix
3) declare it in your config.yml, in the doctrine/orm/entity manager/mapping section (the dir is the directory where you have put the modified definitions):
doctrine:
orm:
...
entity_managers:
default:
mappings:
...
JMSPaymentCoreBundle:
mapping: true
type: xml
dir: "%kernel.root_dir%/Resources/JMSPayment/doctrine"
alias: ~
prefix: JMS\Payment\CoreBundle\Entity
is_bundle: false
tested with Symfony 6 :
Create a class that extends Doctrine's UnderscoreNamingStrategy and handles the prefix :
<?php
# src/Doctrine/PrefixedNamingStrategy.php
namespace App\Doctrine;
use Doctrine\ORM\Mapping\UnderscoreNamingStrategy;
class PrefixedNamingStrategy extends UnderscoreNamingStrategy
{
private const PREFIX = 'sf';
public function classToTableName($className)
{
$underscoreTableName = parent::classToTableName($className);
return self::PREFIX . '_' . $underscoreTableName;
}
}
and configure doctrine to use it :
# config/packages/doctrine.yaml
doctrine:
orm:
naming_strategy: 'App\Doctrine\PrefixedNamingStrategy'
#simshaun answer is good, but there is a problem with Many-to-Many relationships and inheritance.
If you have a parent class User and a child class Employee, and the Employee own a Many-to-Many field $addresses, this field's table will not have a prefix.
That is because of:
if ($classMetadata->isInheritanceTypeSingleTable() && !$classMetadata->isRootEntity()) {
// if we are in an inheritance hierarchy, only apply this once
return;
}
User class (parent)
namespace FooBundle\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Entity()
* #ORM\Table(name="user")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({"user" = "User", "employee" = "\FooBundle\Bar\Entity\Employee"})
*/
class User extends User {
}
Employee class (child)
namespace FooBundle\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Entity()
*/
class Employee extends FooBundle\Bar\Entity\User {
/**
* #var ArrayCollection $addresses
*
* #ORM\ManyToMany(targetEntity="\FooBundle\Bar\Entity\Adress")
* #ORM\JoinTable(name="employee_address",
* joinColumns={#ORM\JoinColumn(name="employee_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="address_id", referencedColumnName="id")}
* )
*/
private $addresses;
}
Address class (relation with Employee)
namespace FooBundle\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Entity()
* #ORM\Table(name="address")
*/
class Address {
}
With the original solution, if you apply pref_ prefixe to this mapping, you will end up with tables :
pref_user
pref_address
employee_address
Solution
A solution can be to modify, in the answer of #simshaun, the point 4 like this:
Create MyBundle\Subscriber\TablePrefixSubscriber.php
<?php
namespace MyBundle\Subscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
class TablePrefixSubscriber implements \Doctrine\Common\EventSubscriber
{
protected $prefix = '';
public function __construct($prefix)
{
$this->prefix = (string) $prefix;
}
public function getSubscribedEvents()
{
return array('loadClassMetadata');
}
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
{
$classMetadata = $args->getClassMetadata();
// Put the Many-yo-Many verification before the "inheritance" verification. Else fields of the child entity are not taken into account
foreach($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
if($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY
&& array_key_exists('name', $classMetadata->associationMappings[$fieldName]['joinTable']) // Check if "joinTable" exists, it can be null if this field is the reverse side of a ManyToMany relationship
&& $mapping['sourceEntity'] == $classMetadata->getName() // If this is not the root entity of an inheritance mapping, but the "child" entity is owning the field, prefix the table.
) {
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
}
}
if($classMetadata->isInheritanceTypeSingleTable() && !$classMetadata->isRootEntity()) {
// if we are in an inheritance hierarchy, only apply this once
return;
}
$classMetadata->setTableName($this->prefix . $classMetadata->getTableName());
}
}
Here we handle the Many-to-Many relationship before verifying if the class is the child of an inheritance, and we add $mapping['sourceEntity'] == $classMetadata->getName() to add the prefix only one time, on the owning entity of the field.

Embed a Collection of Forms Error: Could not determine access type for property

I am trying to embed collection of Tag forms to Service form, according to this tutorial. Tag and Service entities have many-to-many relationship.
Form is rendering correctly. But when I submit form, I get
Could not determine access type for property "tagList"
error. I don't understand why new Tag object is not added to the Service class by calling the addTag() method.
ServiceType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, array(
'label' => 'Title'
))
;
$builder->add('tagList', CollectionType::class, array(
'entry_type' => TagType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
)));
}
Service class
{
....
/**
* #ORM\ManyToMany(targetEntity="Tag", mappedBy="serviceList",cascade={"persist"})
*/
private $tagList;
/**
* #return ArrayCollection
*/
public function getTagList()
{
return $this->tagList;
}
/**
* #param Tag $tag
* #return Service
*/
public function addTag(Tag $tag)
{
if ($this->tagList->contains($tag) == false) {
$this->tagList->add($tag);
$tag->addService($this);
}
}
/**
* #param Tag $tag
* #return Service
*/
public function removeTag(Tag $tag)
{
if ($this->tagList->contains($tag)) {
$this->tagList->removeElement($tag);
$tag->removeService($this);
}
return $this;
}
}
Tag class
{
/**
* #ORM\ManyToMany(targetEntity="Service", inversedBy="tagList")
* #ORM\JoinTable(name="tags_services")
*/
private $serviceList;
/**
* #param Service $service
* #return Tag
*/
public function addService(Service $service)
{
if ($this->serviceList->contains($service) == false) {
$this->serviceList->add($service);
$service->addTag($this);
}
return $this;
}
/**
* #param Service $service
* #return Tag
*/
public function removeService(Service $service)
{
if ($this->serviceList->contains($service)) {
$this->serviceList->removeElement($service);
$service->removeTag($this);
}
return $this;
}
}
ServiceController
public function newAction(Request $request)
{
$service = new Service();
$form = $this->createForm('AppBundle\Form\ServiceType', $service);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($service);
$em->flush();
return $this->redirectToRoute('service_show', array('id' => $service->getId()));
}
return $this->render('AppBundle:Service:new.html.twig', array(
'service' => $service,
'form' => $form->createView(),
));
}
Could you please try to implement code from this URL?
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#owning-and-inverse-side-on-a-manytomany-association
First, please try to change mapped/inverse sides, and remove $service->addTag($this); from Tag::addService method.
Short version:
I just ran into this problem and solved it by adding a setter for the affected property:
Could not determine access type for property "tagList"
public function setTagList(Array $tagList)
{
$this->tagList = $tagList;
}
Long version:
The error message is signaling that Symfony is trying to modify the object's state, but cannot figure out how to actually make the change due to the way its class is set up.
Taking a look at Symfony's internals, we can see that Symfony gives you 5 chances to give it access and picks the best one in this order from top to bottom:
A setter method named setProperty() with one argument:
This is the first thing Symfony checks for and is the most explicit way to achieve this. As far as I'm aware this is the best practice:
class Entity {
protected $tagList;
//...
public function getTagList()
{
return $this->tagList;
}
//...
}
A combined getter and setter in one method with one argument:
It's important to realize that this method will also be accessed by Symfony in order to get the object's state. Since those method calls don't include an argument, the argument in this method must be optional.
class Entity {
protected $tagList;
//...
public function tagList($tags = null)
{
if($reps){
$this->tagList = $tags;
} else {
return $this->tagList;
}
}
//...
}
The affected property being declared as public:
class Entity {
public $tagList;
//... other properties here
}
A __set magic method:
This will affect all properties rather than just the one you intended.
class Entity {
public $tagList;
//...
public function __set($name, $value){
$this->$name = $value;
}
//...
}
A __call magic method (in some cases):
I wasn't able to confirm this, but the internal code suggests this is possible when magic is enabled on PropertyAccessor's construction.
Only using one of the above strategies is required.
Maybe the problem is that Symfony can't access that property?
If you look at where that exception is thrown (writeProperty method in the PropertyAccessor class) it says it can be thrown:
If the property does not exist or is not public.
In the tutorial you mentioned it has property $tags, and method addTag. I'm just guessing here, but maybe there's a convention where it tries to call a method names add($singularForm) and this is failing for you because the property is tagList and the method is addTag.
I'm not 100% sure, but you could try debugging by setting a stop point in that Symfony method to see why it's being thrown.
Maybe you forgot in the __construct() of Service class and Tag class to initialize $tagList and $serviceList like this ?
$this->tagList = new ArrayCollection();
$this->serviceList = new ArrayCollection();
This seems like an error with your constructor. Try this :
public function __construct()
{
$this-> tagList = new \Doctrine\Common\Collections\ArrayCollection();
}
It's a long shot, but looking at your annotations I think the problem might be related to your manyToMany relationship. Try to change the owning side and inverse side (Swap the relationship) unless you specifically need to update from both ends (In that case I think the only solution is to add the objects manually or use oneToMany relationships).
Changes made only to the inverse side of an association are ignored.
Make sure to update both sides of a bidirectional association (or at
least the owning side, from Doctrine’s point of view)
This is a problem related to Doctrine I have suffered before, see:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/unitofwork-associations.html
Based on Symfony 3.3.10
I actually faced this problem many and many times, finally once i discovered where this problem was coming from, depending on the name you give to your entity property it can happen that the adder and the remover for your collection property aren't exactly what you are expecting.
Example: Your entity properity name is "foo" and you would expect the adder to be called "addFoo" and remover "removeFoo", but then all of a sudden the "Could not determine access type for property" appear.
So you start going into fear searching for w/e problems in your code, instead you just have to look this file inside Symfony core files:
vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
Inside this file there's a method called findAdderAndRemover.
Go there with your debugger and you will eventually find out that symfony searches for weird name for your adder/remover, they may actually end with "um" or "on" or "us" depending on the language (human language) you used to name them. Since i'm Italian this happen quite often.
Watch out for that, since the fix may be as simple as changing the name used for your add/remove method inside your entity to make them match with what Symfony core is looking for.
This happens to me when i use bin/console doctrine:generate:entities to create the methods automatically for me
If you are using symfony, and use EntityRepository instead of CollectionType, make sure you use the 'multiple' => true, on your form build, otherwise the input will be for one entity and not for many, therefore it will call the setTagList instead of using the methods addTagList and removeTagList.

Are Value objects associations possible?

I wonder whether it's possible to have relations between entities and value objects or if a third entity is mandatory as relation target. The purpose could be to flag different kind of entities with a common data structure that has it's own business logic. Any idea ?
Update :
Let's say I have a business object to model SCAP CPE namings :
<?php
namespace Scap\Cpe\Naming;
/**
* Cpe22 represents the naming convention in CPE Naming version 2.2
* Accepted values are only CPE URIs
*/
class Cpe22
{
protected $cpe;
public function __construct($cpe)
{
if (! preg_match('/[c][pP][eE]:\/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6}/', $cpe)) {
throw new InvalidNamingException();
}
$this->cpe = $cpe;
}
}
If I want to flag different kind of entities with this VO in a one-to-many way, I can think of 2 different ways :
Entities are directly related to VOs so that the relation identifiers could be the entities references and the VOs themselves (= key composed from referenced identifier and representative VO fields)
Entities are related to third entity types, that embed the VOs so that the relation identifiers are those third entities identifiers
So I wonder if only the second option is available or if the first can somehow be implemented.
You should use a custom mapping type for things like this. For example, it could look something like this:
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform
class Cpe22Type extends Type
{
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getVarcharTypeDeclarationSQL(array('length' => 250));
}
public function convertToPHPVale($value, AbstractPlatform $platform)
{
return new Cpe22($value);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return (string) $value;
}
public function getName()
{
return 'cpe22';
}
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
Obviously, you have to add a __toString() method to your Cpe22 class for this to work.
After registering the type (how you do this depends on your framework, in symfony for example you do it in config.yml) you can simply map your field as type cpe22:
/**
* #ORM\Column(name="my_cpe", type="cpe22")
*/
public $myCpe;

Doctrine2 __constructor not called when using $em->find(); ? How to load entity properly?

I'm learning doctrine2, and having a problem how to call constructor automatically.
For example, in my entity I have
/**
* #Entity
*/
class User{
....
public function __construct() {
exit('in');
}
}
and when I get the object this way:
$userObj = $em->find('User', 1);
I do get that object from database, but constructor is never called.
I want to put some common things in constructor, like validation rules, or even to put sample code from the doctrine documentation like
$this->comments = new ArrayCollection();
This ofcourse works when I create new object in code for creating a user like
$user = new User(); //now constructor works just fine
Now, what is the "proper" way of getting the entity? I doubt I have to call constructor manually each time I user $em->find() with $user0bj->__construct(); ? This would kinda sucks then... Or I should use something other then ->find() to get single entity properly?
I know I can user #PrePersist, and I am using it to actually do validation checks etc.
I guess that I'm probably missing something here, or I'm trying to use constructor in a poor way. Thanks for any explanations and guides!
I'm pretty certain that find or similar isn't expected to call the constructor...
You need to hook into the #PostLoad event.
Why would you want to call the constuctor of already persisted entity? When you need to validate it you should have done the validation or initializations before you have persisted it. So When you call a already persisted entity there is no point to validate it.
The right place to put validation and other initializations is the constructor method of entity.
Eg.
/**
* #Entity
*/
class User{
protected $name;
public function __construct($name) {
if (isset($name)) {
//** validate the name here */
$this->name=$name;
} else {
throw new Exception("no user name set!");
}
}
}
According to the doctrine2 documentation Doctrine2 never calls __construct() method of entities.
http://www.doctrine-project.org/docs/orm/2.0/en/reference/architecture.html?highlight=construct
doctrine uses reflection to instantiate your object without invoking your constructor.
Since PHP 5.4 , you can use reflection to instanciate a class without
calling the constructor using
ReflectionClass::newInstanceWithoutConstructor
the instantiator of doctrine use it like :
private function buildFactory(string $className) : callable
{
$reflectionClass = $this->getReflectionClass($className);
if ($this->isInstantiableViaReflection($reflectionClass)) {
return [$reflectionClass, 'newInstanceWithoutConstructor'];
}
$serializedString = sprintf(
'%s:%d:"%s":0:{}',
is_subclass_of($className, Serializable::class) ? self::SERIALIZATION_FORMAT_USE_UNSERIALIZER : self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER,
strlen($className),
$className
);
$this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);
return static function () use ($serializedString) {
return unserialize($serializedString);
};
}
Doctrine ORM will "rewrite" your class, it generate a new class that implement \Doctrine\ORM\Proxy\Proxy
And it rewrite the construct method:
/**
* #param \Closure $initializer
* #param \Closure $cloner
*/
public function __construct($initializer = null, $cloner = null)
{
$this->__initializer__ = $initializer;
$this->__cloner__ = $cloner;
}
You can see it inside the cache folder ${CACHE}/doctrine/orm/Proxies.
You will need both #ORM\HasLifecycleCallbacks on the class + #ORM\PostLoad on a specific function of your choice.
Beware! If you put it on the constructor it will override loaded database data!
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table(name="dossier")
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks
*/
class Dossier
{
// ...
/**
* The normal constructor stays as usual
*/
public function __construct()
{
$this->takenActions = new ArrayCollection();
$this->classifications = new ArrayCollection();
$this->dossierProblems = new ArrayCollection();
$this->internalNotes = new ArrayCollection();
}
/**
* Triggers after the entity has been loaded in the EntityManager (e.g. Doctrine's ->find() etc...)
* The constructor does not get called. Some variables still need a default value
* Must be in combination with "ORM\HasLifecycleCallbacks" on the class
*
* #ORM\PostLoad
*/
public function postLoadCallback(): void
{
// Only put a default value when it has none yet
if (!$this->dossierProblems)
$this->dossierProblems = new ArrayCollection();
if (!$this->internalNotes)
$this->internalNotes = new ArrayCollection();
}
// ...
}