Asked  7 Months ago    Answers:  5   Viewed   37 times

Let's suppose I retrieve an entity $e and modify its state with setters:

$e->setFoo('a');
$e->setBar('b');

Is there any possibility to retrieve an array of fields that have been changed?

In case of my example I'd like to retrieve foo => a, bar => b as a result

PS: yes, I know I can modify all the accessors and implement this feature manually, but I'm looking for some handy way of doing this

 Answers

31

You can use DoctrineORMEntityManager#getUnitOfWork to get a DoctrineORMUnitOfWork.

Then just trigger changeset computation (works only on managed entities) via DoctrineORMUnitOfWork#computeChangeSets().

You can use also similar methods like DoctrineORMUnitOfWork#recomputeSingleEntityChangeSet(DoctrineORMClassMetadata $meta, $entity) if you know exactly what you want to check without iterating over the entire object graph.

After that you can use DoctrineORMUnitOfWork#getEntityChangeSet($entity) to retrieve all changes to your object.

Putting it together:

$entity = $em->find('MyEntity', 1);
$entity->setTitle('Changed Title!');
$uow = $em->getUnitOfWork();
$uow->computeChangeSets(); // do not compute changes if inside a listener
$changeset = $uow->getEntityChangeSet($entity);

Note. If trying to get the updated fields inside a preUpdate listener, don't recompute change set, as it has already been done. Simply call the getEntityChangeSet to get all of the changes made to the entity.

Warning: As explained in the comments, this solution should not be used outside of Doctrine event listeners. This will break Doctrine's behavior.

Wednesday, March 31, 2021
 
erotsppa
answered 7 Months ago
100

I would say that technically it is not a bug as clear() works as described in the documentation, see Doctrine2 API, documentation and source code (current version).

The clear() method is just a way to detach() all entities or entities of a specified type. It can be thought as a "Multi-Detach", its purpose does not extend past detaching.

When detaching all entities using clear() Doctrine detaches the entities using the most efficient method possible. In the process the Identity Map Array is set to an empty array(). This will give the appearance of what I believe you are referring to as cleared.

$entityManager->clear();
$identity = $entityManager->getUnitOfWork()->getIdentityMap(); 
//This will return a an empty array() to $identity
//therefore $identity['RezaMyBundleEntityListItem'] would be undefined

If we assume that data was retrieved for an entity of RezaMyBundleEntityListItem. Then in the following example we can see that the unit of work has at least 1 RezaMyBundleEntityListItem object.

$identity = $entityManager->getUnitOfWork()->getIdentityMap();
$count = count($identity['RezaMyBundleEntityListItem']);
// $count would be > 0;

However, when you use clear($entityName) and clear by entity type, the cleared/detached entities are removed from the unit of work, it is only the array key [$entityName] that remains, not any of the objects.

$entityManager->clear('RezaMyBundleEntityListItem');
$identity = $entityManager->getUnitOfWork()->getIdentityMap(); 
$count = count($identity['RezaMyBundleEntityListItem']);
//$count would be == 0. All Objects cleared/detached.

This functionality is all that is specified by the documentation.

I do think a feature request is in order, to make it work more consistently. When invoking clear($entityName) Doctrine should unset() the remaining key thereby making it undefined (cleared). This would allow us to more easily write code that would work whether we used clear() or clear($entityName).

Wednesday, March 31, 2021
 
jsuissa
answered 7 Months ago
71

You need to define a literal tag for % before and/or after the value you want to search; in this case you won't even need to have single quotation before and after your phrase:

$qb = $this->em->createQueryBuilder();
$qb->select('u')
    ->from("myBundle:Entity", 'u')
    ->where($qb->expr()->like('u.tags', $qb->expr()->literal("%$tag%")))
$result=$qb->getQuery()->getResult();
return $result;

You can follow a list of all Doctrine expr class

Saturday, May 29, 2021
 
HexaGridBrain
answered 5 Months ago
36

Short of iterating through the iterable and counting the number of iterations, no. That's what makes it an iterable and not a list. This isn't really even a python-specific problem. Look at the classic linked-list data structure. Finding the length is an O(n) operation that involves iterating the whole list to find the number of elements.

As mcrute mentioned above, you can probably reduce your function to:

def count_iterable(i):
    return sum(1 for e in i)

Of course, if you're defining your own iterable object you can always implement __len__ yourself and keep an element count somewhere.

Wednesday, June 2, 2021
 
Zulakis
answered 5 Months ago
13

So, all my searching didn't yield a short solution. The answer to the question seems to be: no, there is no buit-in way to get the current date (or datetime) as a variable in the template.

In case others are searching for this topic, I'll try to give a summary of the possible workarounds that I can up with and that were suggested by other users.


  1. I could pass a context variable to the template from my view. In class-based views that could look like this (it is even an example in the docs):

    # file 'my_app/views.py'
    from django.utils import timezone as tz
    from django.views.generic import ListView
    
    class MyView(ListView)
        ...
    
        def get_context_data(self, **kwargs):
            ctx = super().get_context_data(**kwargs)
    
            now = tz.now()
            ctx['now'] = now
            ctx['today'] = tz.localtime(now).date()
    
            return ctx
    
  2. I could create a custom context processor that loads that variable to every template. In class-based views that could look like this:

    # file 'context_processors.py'
    from django.utils import timezone as tz
    
    def now_and_today(request):
        now = tz.now()
        return {
            'now': now,
            'today': tz.localtime(now).date(),
        }
    
    # file 'settings.py'
    ...
    TEMPLATES = [
        {
            ...
            'OPTIONS': {
                'context_processors': [
                    ...
                    'context_processors.today_and_now',
                ],
            },
        },
    ]
    ...
    
  3. I could create a custom template tag, like this:

    # file 'my_app/custom_template_tags/custom_time_tags.py'
    from django.utils import timezone as tz
    from django import template
    register = template.Library()
    
    @register.simple_tag
    def get_now(request):
        return tz.now()
    
    @register.simple_tag
    def get_today(request):
        return tz.localtime(tz.now()).date()
    

    To be used like this:

    {% load 'custom_time_tags' %}
    
    {% get_today as today %}
    {% for per in person_list %}
        {% if per.brith_date > today %}
            <p>{{ per.name }} is from the future!!<p>
        {% endif %}
    {% endfor %}
    
  4. I could also add a property (or even a cached_property) to the model:

    # file 'models.py'
    from django.db import models
    from django.utils import timezone as tz
    from django.utils.functional import cached_property
    
    class Person(models.Model):
        ...
    
        @cached_property
        def is_from_future(self):
            # careful: for long-lived instances do not use 'cached_property' as
            # the time of 'now' might not be right later
            if self.birth_date > tz.localtime(tz.now()).date():
                return True
    
            return False
    
  5. And last but not least, I could just do the processing in the view and add a property to the elements:

    # file 'my_app/views.py'
    from django.utils import timezone as tz
    
    def person_list(request):
        today = tz.localtime(tz.now()).date()
    
        person_list = []
        for p in Person.objects.all():
            p.is_from_future = self.birth_date > today
            person_list.append(p)
    
        return render(request, 'some_template.html', {'person_list': person_list})
    
Saturday, July 31, 2021
 
Success Man
answered 3 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share