Symfony - How to add ORM index to the Trait - symfony

I have several entities (lets say 5), which use this trait, and I want to add an index to uuid field via the trait,
so I want just use the trait in some specific entity and as a result have an indexed field in the entity
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;
trait UuidTrait {
/**
* #ORM\Column(type="string", nullable=true, options={"default": null})
*/
protected $uuid;
public function getUuid()
{
//common code
}
public function generateUuid()
{
//common code
}
public function setUuid($uuid)
{
//common code
}
}

There is no direct annotation supported to add an index directly at the column definition.
And a trait is pretty much a plain include of a file.
http://doctrine-orm.readthedocs.io/en/latest/reference/annotations-reference.html#column
There is also a case open on github
https://github.com/doctrine/doctrine2/issues/6249
There is a workaround that could work, but it might not fit your model requirements.
You can define the column as unique, which will create an index.
But you will have to make the column non-nullable.
That said, from an optimization point of view, such a setting belongs at the table level, where it will be easier to have an overview of your existing indexes and judge if you have to fine-tune one or more of them.

Related

Adding data to returned Entities in Symfony

Question really relates to best practice and whether what I have in my head is possible. I am querying for a collection of Member entities using a Repository function, for example (simplified)
/**
* #return Query
*/
public function findAllMembersOrderedByNameResult()
{
return $this->createQueryBuilder('m')
->orderBy('m.lastName', 'ASC')
->getQuery()
->getResult()
;
}
I am calling this in my Controller:
$members = $em->getRepository(Member::class)->findAllMembersOrderedByNameResult();
Now I am passing the result this to my twig template file and can loop through this and display information about each member as part of a foreach loop:
{% for member in members %}
{{member.firstName}}
{% endfor %}
This obviously works fine, however I have a need now to add some additional data to each member to pass to the twig template. So for example using the DateOfBirth in the Member entity I want to run this through a function to determine the age of the member to display in the Twig template. So at the moment I am making this call in the Controller, and then passing it over to the template by creating a MembersArray, looping through the returned $members Collection and adding in the new age and whole Member result as two separate values in the array, for example:
foreach($members as $member)
{
$membersArray[] = array(
'member' => $member,
'age' => $age
)
}
This does work, however in my Twig template I now have to use
{% for member in membersArray %}
{{member.member.firstName}}
{{member.age}
{% endfor %}
It would be much nicer to be able to just add age into the Collection without creating the array so I can just use member.firstName and member.age but for the life of me can't find a way how without having to loop through all values manually and set them to a new array, which seems a huge waste of code.
I will want to add more than just age, this is just a simplified example. Using Symfony 4.4 and PHP 7.3 in case anything that would help requires it
Edit: Entity structure as requested (cutdown):
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\MemberRepository")
*/
class Member
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $firstName;
/**
* #ORM\Column(type="date", nullable=true)
*/
private $dateOfBirth;
...
The simplest way would be adding custom getter methods to an entity itself, like:
public function getAge()
{
return (new DateTime())->diff($this->dateOfBirth)->y;
}
Then you can calculate the age in a twig template by calling member.age.
But usually, it's not recommended to have any logic in an entity itself.
Another way is using twig itself to format data. I would say it is preferable with the age example. Because age looks more like the view format of the dataOfBirth field than extra data, you can use a twig filter to calculate that. The built-in like {{ date().diff(member.dateOfBirgth)).format('%y') }}, or define a custom one with a twig extension, so the syntax would be more straightforward, like {{ member.dateOfBirgth|age }} or even {{ member|age }}.
In case you have to reuse formatting not only in twig but also in some services and don't want to put logic into an entity - you can decouple twig extension from the previous example to use the shared service that is responsible for formatting age. Other parts of the system can use the same formatting service.
Also, it's a common practice to put new methods to work with an entity to a custom entity manager service, like MemberManager. Usually, we use entity manager to manipulate entities, but you can add there the method to format age, like MemberManager->getAge($member). This would violate the single responsibility principle, so I can't recommend it.
If you are looking for extending doctrine entities with some extra methods with listeners or so, it's not possible by doctrine design. Anyway, it's the most, not obvious way.
To summarise, in most cases, when the custom function can be considered as a formatting one, I would recommend using the second option, with decoupling it to the third option whenever you have to reuse the formatting code. But if the project isn't complex, the first option also worth checking, as it's the simplest one.

How do I implement dynamic (i.e. not cached) Doctrine Asserts in Symfony2?

I have a Doctrine-Entity in my Symfony2-Project, which uses a custom Assert/Constraint to check, if a given date value is before and/or after a given date. This looks like the following simplified code:
In my entity class:
/**
* #var \DateTime
*
* #ORM\Column(name="entry_entered_at", type="date", nullable=true)
* #AppBundleAssert\DateRangeConstraint(max = "today")
*/
private $entryEnteredAt;
The relevant snippet of the corresponding DateRangeConstraint-class:
new \DateTime($this->max)
As you can see, I want to check, if a date is before today. The \DateTime-constructor is able to resolve this to a DateTime-object of today. Nice thing, works fine.
The problem
But it turns out, that Symfony2 caches all those Doctrine-annotations, so today is always resolved to the day, the cache was lastly cleared and my constraint produces nice form errors.
As a workaround for now, I clear the cache on a daily basis, but I need a better solution.
The question
So the question is, what would you suggest, how to implement such a dynamic assert/constraint in Symfony2?
I could implement the constraint inside the form, but it should be in the domain of the entity.
Edit:
I posted as answer and marked it as solution.
The solution and some answers
It turned out, that the built in Range validator is also able to validate a date-range. So I don't need my custom validator at all.
Digging a bit deeper into the built in Range constraint and the base Constraint class gives the reason, why the built in validators can use dynamic parameters like today, but not my incorrect implemented custom validator. The Constraint base class has a __sleep() method that just stores the object vars and its current values on serialization. Thus, when we don't reinitialize the object with a custom __wakeup() method, which would be a false workaround, we only get the cached parameters.
So besides the fact, that the builtin Range constraint already solves my problem, I simply should have done my dynamic new \DateTime($constraint->max) stuff inside the custom DateRangeConstraintValidator and not the cached custom DateRangeConstraint. Just have a look into Symfony\Component\Validator\Constraints\Range and Symfony\Component\Validator\Constraints\RangeValidator to see this in action.
Lessons learned
Your custom Constraint class will be serialized and cached and thus shouldn't do any dynamic things. Just validate the options and define the messages and stuff. Your dynamic validation things (and especially the initialization of dynamic parameters) must be done within your custom ConstraintValidator class.
I suggest you to look at Custom validator, especially Class Constraint Validator.
I won't copy paste the whole code, just the parts which you will have to change.
Extends the Constraint class.
src/Acme/DemoBundle/Validator/Constraints/CheckEntryEnteredAt.php
<?php
namespace Acme\DemoBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class CheckEntryEnteredAt extends Constraint
{
public $message = 'Your error message.';
public function validatedBy()
{
return 'CheckEntryEnteredAtValidator';
}
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
Define the validator by extending the ConstraintValidator class, entryEnteredAt is the field you want to check:
src/Acme/DemoBundle/Validator/Constraints/CheckEntryEnteredAtValidator.php
namespace Acme\DemoBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class CheckEntryEnteredAtValidator extends ConstraintValidator
{
public function validate($entity, Constraint $constraint)
{
$today = new \Datetime('today'); // = midnight
if ($entity->entryEnteredAt < $today) {
$this->context->addViolationAt('entryEnteredAt',
$constraint->message, array(), null);
}
}
}
Use the validator:
src/Acme/DemoBundle/Resources/config/validation.yml
Acme\DemoBundle\Entity\AcmeEntity:
constraints:
- Acme\DemoBundle\Validator\Constraints\CheckEntryEnteredAt: ~
(adapted from a previous answer)
public function __construct()
{
$this->entryEnteredAt = new \DateTime();
}
is something like that a solution for your use case? (on new YourEntity() you'll have a today date set for the entryEnteredAt property)
You could also use LifecycleCallbacks, here is an exemple with preUpdate (there is some more, like PrePersist):
on top of your class entity:
* #ORM\HasLifecycleCallbacks()
and
/**
* Set updatedAt
*
* #ORM\PreUpdate
*/
public function setUpdatedAt()
{
$this->updatedAt = new \DateTime();
}

Symfony2 - Share Entity Between Bundles with different relationships

How do you share an entity between multiple bundles with different relationships?
For example both the ZooAnimalBundle and FarmAnimalBundle need a User Entity. A third Bundle AccountUserBundle has the User Entity.
In both the Zoo and Farm AnimalBundles I create a User Entity like so:
use Account\UserBundle\Entity\User as BaseUser;
class User extends BaseUser
{
}
I then have a Hospital entity in Zoo:
class Hospital {
/**
* #ORM\ManyToMany(targetEntity="Zoo\AnaimalBundle\Entity\User")
* #ORM\JoinTable(name="users_zoo_animals")
*/
protected $users;
And a Room entity in Farm:
class Room {
/**
* #ORM\ManyToMany(targetEntity="Farm\AnaimalBundle\Entity\User")
* #ORM\JoinTable(name="users_farm_animals")
*/
protected $users;
Everything works so far in that I can call Zoo.Room->getUsers() or Farm.Hospital->getUsers()
However the problem is I'm not sure on how to set up the inverse relationship in their respective User entities.
If for example I update the FarmAnimal User Entity and run doctrine:generate:entities
/**
* #ORM\Entity
*/
class User extends BaseUser
{
/**
* #ORM\ManyToMany(targetEntity="Room", mappedBy="users", cascade={"persist"})
*/
protected $rooms;
}
It will copy the protected $properties from BaseUser and create all the set and get methods which is not what I want. What is the correct way of setting up these relationships?
Update
If you don't setup the inverse relationship, how would you select all users where hospital.id = 1
$qb = $this->getEntityManager()->createQueryBuilder()
->select(
'u'
)
->from('Account\UserBundle\Entity\User','u')
->leftJoin('u.hospitals', 'h')
->andWhere('h.id = :hospital_id')
->setParameter('hospital_id',$hospital_id);
This gives the error:
Class Account\UserBundle\Entity\User has no association named hospitals
I know I could select from hospital and join user because that relationship does exist but I need to select users because I am using them with Doctrine\ORM\Tools\Pagination\Paginator
The query would be
$qb = $this->createQueryBuilder('a')
->select(
'h', 'u'
)
->leftJoin('h.users', 'u')
The problem with this is Paginator only sees one result Hospital because the Users are attached to it.
You can define abstract entity dependencies and implement them with other bundles.
First, each of the bundles depending on a user entity should define a User interface. For example:
namespace Foo\BarBundle\Entity;
interface UserInterface
{
public function getId();
public function getEmail();
// other getters
}
Then, in each entity depending on the user, define the relationship, e.g.:
namespace Foo\BarBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
*/
class Something
{
/**
* #ORM\ManyToOne(targetEntity="UserInterface")
* #Assert\NotNull
*/
protected $User;
// add other fields as required
}
Now you need to register the User entity as an implementation of the UserInterfaces:
namespace Foo\UserBundle\Entity;
use Foo\BarBundle\Entity\UserInterface as BarUserInterface;
use Foo\FoobarBundle\Entity\UserInterface as FoobarUserInterface;
/**
* #ORM\Entity
*/
class User implements BarUserInterface, FoobarUserInterface
{
// implement the demanded methods
}
Then add the following to app/config/config.yml:
doctrine:
orm:
resolve_target_entities:
Foo\BarBundle\Entity\UserInterface: Foo\UserBundle\Entity\User
Foo\FooarBundle\Entity\UserInterface: Foo\UserBundle\Entity\User
(Heads up: there will usually already be a doctrine.orm node which you'll have to extend.)
This is not a perfect solution, because you cannot really say which fields the user entity should have. On the other hand, it's strictly OOP, as you don't have to know about internals of the User implementation – you just need it to return the right values.
Creating multiple definitions of the account is the wrong way to do it, unless you want to create 3 seperate user tables (even then it's better not to do it this way).
Really you want your other entities to map to the your user entity in the account bundle.
I.e.,
class Hospital {
/**
* #ORM\ManyToMany(targetEntity="Zoo\AccountBundle\Entity\User")
*/
protected $users;
Now, there is no need to create the inverse relationship. In fact, this is a bad practice since you have a bi-directional dependency. Users don't know about hospitals, but hospital knows about it's users. Now, any bundle can map to the user entity and reuse it.

association mapping when one entity isn't managed by Doctrine

I have 2 entities in a one-to-one association. The first, Person, is stored in a MySQL database and handled by Doctrine. The second, AdUserRecord, describes an ActiveDirectory user record. It is read-only. It does not need to know about Person. Also, AdUserRecord properties should never be stored in the MySQL db for privacy reasons.
An AdUserRecord is retrieved using a service, AdSearcher, which can search by samaccountname or objectGUID. Whenever a search is successful, the service checks to see if there is a corresponding Person record and creates one if there is not. That works fine.
My problem occurs when I start with a Person object. Mostly, I don't need to access a Person's AdUserRecord so I'd prefer not to query Active Directory unless it's required. That means, I think, that Person::getAdrecord() needs to have access to the AdSearcher service. Something like this:
public function getAdrecord(){
if($this->adrecord) return $this->adrecord;
$searcher = ???; //get AdSearcher service somehow
$record = $search->getRecordByUserGuid($this->ad_guid);
if(!$record) throw new \Exception('this person no longer exists');
$this->adrecord = $record;
return $this->adrecord;
}
I've been reading the Symfony docs pretty assiduously, but I'm still stumped.
Questions
how do I get a service into an entity? Should it be injected via the constructor, or just where it's needed, in the getter? If it only occurs in the getter, do I have to inject it or is there a way to import it?
is adding a service to an entity the canonical way of handling these types of situations? Would it be preferable to build an entity manager for AdUserRecords?
what interfaces do I need to implement if I have to build an entity manager?
Person class
namespace ACRD\DefaultBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use ACRD\DefaultBundle\Entity\AdUserRecord;
/**
* #ORM\Entity
* #Orm\Table(name="person")
*
*/
class Person {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="AD_guid", type="string", length=36, unique=true)
*/
protected $ad_guid;
/**
* #var AdUserRecord
*/
protected $adrecord;
//usual getters and setters
}
It looks like Doctrine's postLoad event is the best solution.
// src/Acme/DemoBundle/EventListener/ActiveDirectorySubscriber.php
namespace Acme\DemoBundle\EventListener;
use Acme\DemoBundle\Model\AdAwareInterface;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
// for doctrine 2.4: Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Symfony\Component\DependencyInjection\ContainerAware
class ActiveDirectorySubscriber extends ContainerAware implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
'postLoad',
);
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!($entity instanceof AdAwareInterface)) {
return:
}
$adSearcher = $this->getContainer()->get('acme_demo.ad_searcher');
if ($adPerson = $adSearcher->find($entity->getAdGuid())) {
$entity->setAdPerson($adPerson);
}
}
}
You also mentioned that most of the time you don't need to use the active directory stuff. Before optimizing I highly suggest you actually measure how much of a performance impact there is. If, however, you do notice a performance problem, consider using a proxy object to mitigate the AdPerson searching right to the point where you actually need something from it.
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!($entity instanceof AdAwareInterface)) {
return:
}
$adSearcher = $this->getContainer()->get('acme_demo.ad_searcher');
$entity->setAdPerson(new AdPersonProxy($adSearcher));
}
The AdPersonProxy would basically extend from your AdPerson class, wrap each and every public method with a call to load the actual AdPerson object and then act as a facade between the two. Consider the following implications before you start coding though:
it adds complexity to your codebase (the more code, the more there is to maintain);
it will be a pain to debug - for example you might get an exception inside your
template that will leave you scratching your head for a long time (been there,
done that);
The bottom line is that in theory services should (mostly) not be injected inside entities.
Regarding your third question:
EntityManagers implement Doctrine/Common/Persistence/ObjectManager - have a look at the interface on github.
Further:
a somewhat clean implementation would be similar to the Document<->Entity mapping (called references) provided by gedmo/doctrine-extensions.
Take a glimpse at the documentation to see how it works here and here.
If that's what you want start diving into the code of the ReferenceListener :)

Symfony2: Is it possible to place validation annotations on an interface?

Say I have the interface:
namespace Acme\Bundle\FooBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
interface IFoo {
/**
* #Assert\NotBlank
* #Assert\MaxLength(3000)
*/
function getBody();
}
Two classes implement the interface and I want those classes to also be able to make use of the validation annotations on the getBody declaration. (i.e. I don't want to have to duplicate the validation code in each subclass implementing IFoo since it violates DRY).
Doing this however gives me the following exception:
Trying to invoke abstract method Acme\Bundle\FooBundle\Entity\IFoo::getBody()
Does anyone know if this is possible, or any workarounds?
Seems that you can't annotate an interface, there is a ticket open on github for this issue:
https://github.com/symfony/symfony/issues/2841
I don't think you can use validation for method declarations as they are supposed to be used with properties. You could use an abstract mapped superclass for this, though.
Something along the lines of
/** #MappedSuperclass */
abstract class Foo implements FooInterface
{
/** #Column(type="string")
* #Assert\NotBlank
* #Assert\MaxLength(3000)
*/
protected function $body;
// rest of the class
}
You could then extend your other classes from this one.

Resources