Disable Doctrine Timestampable auto-updating the `updatedAt` field on certain update - symfony

In a Symfony2 project, I have a Doctrine entity that has a datetime field, called lastAccessed. Also, the entity uses Timestampable on updatedAt field.
namespace AppBundle\Entity;
use
Doctrine\ORM\Mapping as ORM,
Gedmo\Mapping\Annotation as Gedmo
;
class MyEntity {
/**
* #ORM\Column(type="datetime")
*/
private $lastAccessed;
/**
* #ORM\Column(type="datetime")
* #Gedmo\Timestampable(on="update")
*/
private $updatedAt;
}
I need to update the field lastAccessed without also updating the updatedAt field. How can I do that?

I just stumbled upon this as well and came up with this solution:
public function disableTimestampable()
{
$eventManager = $this->getEntityManager()->getEventManager();
foreach ($eventManager->getListeners('onFlush') as $listener) {
if ($listener instanceof \Gedmo\Timestampable\TimestampableListener) {
$eventManager->removeEventSubscriber($listener);
break;
}
}
}
Something very similar can be used to disable the blamable behavior as well of course.

There is also an easy way (or more proper way) how to do it, just override listener.
first create interface which will be implemented by entity
interface TimestampableCancelInterface
{
public function isTimestampableCanceled(): bool;
}
than extend Timestampable listener and override updateField.
this way we can disable all all events or with cancelTimestampable define custom rules for cancellation based on entity state.
class TimestampableListener extends \Gedmo\Timestampable\TimestampableListener
{
protected function updateField($object, $eventAdapter, $meta, $field)
{
/** #var \Doctrine\Orm\Mapping\ClassMetadata $meta */
$property = $meta->getReflectionProperty($field);
$newValue = $this->getFieldValue($meta, $field, $eventAdapter);
if (!$this->isTimestampableCanceled($object)) {
$property->setValue($object, $newValue);
}
}
private function isTimestampableCanceled($object): bool
{
if(!$object instanceof TimestampableCancelInterface){
return false;
}
return $object->isTimestampableCanceled();
}
}
implement interface. Most simple way is to just set property for this
private $isTimestampableCanceled = false;
public function cancelTimestampable(bool $cancel = true): void
{
$this->isTimestampableCanceled = $cancel;
}
public function isTimestampableCanceled():bool {
return $this->isTimestampableCanceled;
}
or define rules like you want
last thing is to not set default listener but ours.
I'm using symfony so:
stof_doctrine_extensions:
orm:
default:
timestampable: true
class:
timestampable: <Namespace>\TimestampableListener
Than you can just do
$entity = new Entity;
$entity->cancelTimestampable(true)
$em->persist($entity);
$em->flush(); // and you will get constraint violation since createdAt is not null :D
This way can be timestamps disabled per single entity not for whole onFlush. Also custom behavior is easy to apply based on entity state.

Timestampable is just doctrine behavior so is executed every time when you use an ORM.
In my opinion the simplest way is just using DBAL layer and raw sql query.
For example:
$sql = "UPDATE my_table set last_accessed = :lastAccess where id = :id";
//set parameters
$params['lastAccess'] = new \DateTime();
$params['id'] = $some_id;
$stmt = $this->entityManager->getConnection()->prepare($sql);
$stmt->execute($params);
You can of course put it into proper repository class

Related

API Platform documentation for custom normalizer

I am using API Platform and I followed this tutorial to add a custom serialized field which relies on an external service. The avatar property needs to be exposed using the Packages class.
<?php
namespace App\Serializer;
use App\Entity\User;
use Symfony\Component\Asset\Packages;
use Symfony\Component\HttpFoundation\UrlHelper;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class UserNormalizer implements ContextAwareNormalizerInterface
{
/**
* #var Packages
*/
private $packages;
/**
* #var UrlHelper
*/
private $urlHelper;
/**
* #var ObjectNormalizer
*/
private $normalizer;
public function __construct(Packages $packages, UrlHelper $urlHelper, ObjectNormalizer $normalizer)
{
$this->packages = $packages;
$this->normalizer = $normalizer;
$this->urlHelper = $urlHelper;
}
public function normalize($user, $format = null, array $context = [])
{
/** #var array */
$data = $this->normalizer->normalize($user, $format, $context);
$avatar = null;
if ($user->getAvatarFilename()) {
$path = $this->packages->getUrl('uploads/avatars/'.$user->getAvatarFilename());
$avatar = $this->urlHelper->getAbsoluteUrl($path);
}
$data['avatar'] = $avatar;
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof User;
}
}
The problem is that this property doesn't appear in the documentation as it's added by the custom normalizer. How can I add documentation for it (eg. type, example etc...)?
if this still relevant for you or somebody else:
You can add a custom field to the openapi model with this: https://api-platform.com/docs/core/swagger/#overriding-the-openapi-specification
You need to add $avatar field to your entity.
Like you suggest in the comments you could add a not mapped property to your entity and document it in the annotations, and yes it's hacky like they said here!... it is suggested in the SymfonyCast tutorials
Just remember the downside to this approach: our documentation has no
idea that this isMe field exists. If we refresh this page and open the
docs for fetching a single User... yep! There's no mention of isMe. Of
course, you could add a public function isMe() in User, put it in the
user:read group, always return false, then override the isMe key in
your normalizer with the real value. That would give you the custom
field and the docs. But sheesh... that's... getting kinda hacky.

get autogenerated id for additional field, using post-persist

i have a product with an autogenarete id and also have a productcode field, which grabs values based on user choices combined with the autogenated key to make the productcode. However i cannot grab the autogenate id when inserting a new product.
I used first prepersist & preupdate but that doesn't grab the id when inserting a new product. only when updating it grabs the id
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function setProductcode()
{
$option1 = $this->option1;
$option2 = $this->option2;
$id = $this->id;
$whole = $option1.''.$option2.''.$id;
$this->productcode = $whole;
}
i try to use postpersist, and changed my field to be nullablae true but it saves the productcode as null.
/**
* #var string
*
* #ORM\Column(type="string", length=191, unique=true, nullable=true)
*/
private $productcode;
I used postload and postpersist together and it does show the productcode as output.. but it isn't save it the db.
* #ORM\PostLoad
* #ORM\PostPersist
How can i grab the id in the entity to put it in additional field? Thanks in advance!
edit
I made an easyadminsubcriber and it works when i use the pre_persist return.
However the code below is updated to post_persist. but i have trouble implementing the flush function together with lifecycleeventargs.
i got the following error back
Argument 2 passed to App\EventSubscriber\EasyAdminSubscriber::setProductcode() must be an instance of Doctrine\Common\Persistence\Event\LifecycleEventArgs, string given, called in
below is my post_persist code
<?php
# src/EventSubscriber/EasyAdminSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use App\Entity\Product;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
class EasyAdminSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
'easy_admin.post_persist' => array('setProductcode'),
);
}
/**
* #param LifecycleEventArgs $args
*/
public function setProductcode(GenericEvent $event, LifecycleEventArgs $args)
{
$entityManager = $args->getEntityManager();
$entity = $event->getSubject();
if (!($entity instanceof Product)) {
return;
}
$whole = 'yooo';
$entityManager->flush();
$entity->setProductcode($whole);
$event['entity'] = $entity;
}
}
by default, the id is only set, when the entity is flushed to the database. this means, you have to generate your product code after you have flushed the entity and then flush again. doctrine can't use some fancy magic to determine the id before it actually hears back from the database, so there's not really another way. (if you want to do all of this in-entity, I can't imagine another practical and clean way to do this)
update
you should use PostPersist (while keeping PreUpdate).
The postPersist event occurs for an entity after the entity has been made persistent. It will be invoked after the database insert operations. Generated primary key values are available in the postPersist event. (source)
so, the generated primary key is available there. However, this is only after you flushed the entity. So, you'd have to flush again to write the productcode to the database as well.
create proper event handlers (because "setProductcode" is a setter, not an event handler, at least name-wise)
/**
* PostPersist triggers after the _creation_ of entities in db
* #ORM\PostPersist
*/
public function postPersist(LifecycleEventArgs $args) {
$this->setProductcode();
// need to flush, so that changes are written to database
$args->getObjectManager()->flush();
}
/**
* PreUpdate triggers before changes are written to db
* #ORM\PreUpdate
*/
public function preUpdate() {
$this->setProductcode();
// don't need to flush, this happens before the database calls
}
(see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-callbacks-event-argument for further information)
(disclaimer: this answer was heavily edited since it first was created, leaving the connected comments partly without relevant references)
Do you really need to persist the productcode if it is just a concatenation of other columns? What about just using an efficient getter?
public function getProductcode()
{
if(!empty($this->productcode)){
return $this->productcode;
}
if(empty($this->id)){
return "to be determined";
}
$this->productcode = $this->option1 . $this->option2 . $this->id;
return $this->productcode;
}
Alright so i have now 2 solutions to set the autogenerate id in another field (by not using the controller). First one is directly in entity file itself as shown in #jakumi answer.
public function setProductcode()
{
$part = $this->producttype->gettypenumber();
$id1 = $this->id;
$part = sprintf("%03d", $id1);
$whole = $part1.''.$part2;
return $this->productcode= $whole;
}
/**
* PostPersist triggers after the _creation_ of entities in db
* #ORM\PostPersist
*/
public function postPersist(LifecycleEventArgs $args) {
$this->setPoductcode();
// need to flush, so that changes are written to database
$args->getObjectManager()->flush();
}
/**
* PreUpdate triggers before changes are written to db
* #ORM\PreUpdate
*/
public function preUpdate() {
$this->setProductcode();
// don't need to flush, this happens before the database calls
}
Another solutions is to use the eventsubscriber.
<?php
# src/EventSubscriber/EasyAdminSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Product;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
class EasyAdminSubscriber implements EventSubscriberInterface
{
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
public static function getSubscribedEvents()
{
return array(
'easy_admin.post_persist' => array('setProductcode'),
);
}
public function setProductcode(GenericEvent $event)
{
$entity = $event->getSubject();
if (!($entity instanceof Product)) {
return;
}
$this->em->flush();
$entity->setProductcode();
$this->em->flush();
}
}
and my entity code with postpersist & preupdate
/**
* #ORM\PostPersist
* #ORM\PreUpdate
*/
public function setProductcode()
{
$part1 = $entity->getProducttype()->getTypenumber();
$id1 = $entity->getId();
$part2 = sprintf("%03d", $id1);
$whole = $part1.$part2;
$this->productcode = $whole;
}
Thanks #Jakumi for explanation & guidelines for both solutions.

How to run a lookup query inside Doctrine2 Entity with Symfony3

I have a basic Doctrine2 entity, but one of the fields needs some formatting applied to it to turn it from a database primary key, into a user-visible "friendly ID".
I want to put the formatting logic in only one place, so that if it ever changes, it only has to be updated once.
Part of the formatting involves looking up a string from the database and using that as a prefix, as this value will be different for different installations. I am a bit stuck, because within the entity I can't (and probably shouldn't) look up the database to retrieve this prefix.
However I am not sure how else to go about this.
Here is some pseudocode illustrating what I am trying to do:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
// This is also an entity, annotations/getters/setters omitted for brevity.
class Lookup {
protected $key;
protected $value;
}
class Person {
/**
* Database primary key
*
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* Get the person's display ID.
*
* #Serializer\VirtualProperty
* #Serializer\SerializedName("friendlyId")
*/
protected function getFriendlyId()
{
if ($this->person === null) return null;
//$prefix = 'ABC';
// The prefix should be loaded from the DB, somehow
$lookup = $this->getDoctrine()->getRepository('AppBundle:Lookup')->find('USER_PREFIX');
$prefix = $lookup->getValue();
return $prefix . $this->person->getId();
}
}
You could use event listeners using symfony and doctrine and listen to postLoad event by registering the service
services:
person.postload.listener:
class: AppBundle\EventListener\PersonPostLoadListener
tags:
- { name: doctrine.event_listener, event: postLoad }
Now in your listener you will have an access to entity manager
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\Person;
class PersonPostLoadListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof Person) {
return;
}
$entityManager = $args->getEntityManager();
$lookup =$entityManager->getRepository('AppBundle\Entity\Lookup')->findOneBy(array(
'key'=> 'USER_PREFIX'
));
$entity->setFriendlyId($entity->getId().$lookup->getValue());
//echo "<pre>";dump($entity);echo "</pre>";die('Call')
}
}
And in your person entity you need to define an un mapped property for your id and its getter and setter method like
class Person
{
private $friendlyId;
public function getFriendlyId()
{
return $this->friendlyId;
}
public function setFriendlyId($friendlyId)
{
return $this->friendlyId = $friendlyId;
}
}

Symfony validation callback

I'm trying to validate my entity via static callback.
I was able to make it work following the Symfony guide but something isn't clear to me.
public static function validate($object, ExecutionContextInterface $context, $payload)
{
// somehow you have an array of "fake names"
$fakeNames = array(/* ... */);
// check if the name is actually a fake name
if (in_array($object->getFirstName(), $fakeNames)) {
$context->buildViolation('This name sounds totally fake!')
->atPath('firstName')
->addViolation()
;
}
}
It works fine when I populate my $fakeNames array but what if I want to make it "dynamic"? Let's say I want to pick that array from the parameters or from the database or wherever.
How am I supposed to pass stuff (eg. the container or entityManager) to this class from the moment that the constructor doesn't work and it has to be necessarily static?
Of course my approach may be completely wrong but I'm just using the symfony example and few other similar issues found on the internet that I'm trying to adapt to my case.
You can create a Constraint and Validator and register it as service so you can inject entityManager or anything you need, you can read more here:
https://symfony.com/doc/2.8/validation/custom_constraint.html
or if you are on symfony 3.3 it is already a service and you can just typehint it in your constructor:
https://symfony.com/doc/current/validation/custom_constraint.html
This is the solution I was able to find in the end.
It works smoothly and I hope it may be useful for someone else.
I've set the constraint on my validation.yml
User\UserBundle\Entity\Group:
constraints:
- User\UserBundle\Validator\Constraints\Roles\RolesConstraint: ~
Here is my RolesConstraint class
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\Validator\Constraint;
class RolesConstraint extends Constraint
{
/** #var string $message */
public $message = 'The role "{{ role }}" is not recognised.';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
and here is my RolesConstraintValidator class
<?php
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RolesConstraintValidator extends ConstraintValidator
{
/** #var ContainerInterface */
private $containerInterface;
/**
* #param ContainerInterface $containerInterface
*/
public function __construct(ContainerInterface $containerInterface)
{
$this->containerInterface = $containerInterface;
}
/**
* #param \User\UserBundle\Entity\Group $object
* #param Constraint $constraint
*/
public function validate($object, Constraint $constraint)
{
if (!in_array($object->getRole(), $this->containerInterface->getParameter('roles'))) {
$this->context
->buildViolation($constraint->message)
->setParameter('{{ role }}', $object->getRole())
->addViolation();
}
}
}
Essentially, I set up a constraint which, every time a new user user is registered along with the role, that role must be among those set in the parameters. If not, it builds a violation.

Symfony2 - Set default value in entity constructor

I can set a simple default value such as a string or boolean, but I can't find how to set the defualt for an entity.
In my User.php Entity:
/**
* #ORM\ManyToOne(targetEntity="Acme\DemoBundle\Entity\Foo")
*/
protected $foo;
In the constructor I need to set a default for $foo:
public function __construct()
{
parent::__construct();
$this->foo = 1; // set id to 1
}
A Foo object is expected and this passes an integer.
What is the proper way to set a default entity id?
I think you're better to set it inside a PrePersist event.
In User.php:
use Doctrine\ORM\Mapping as ORM;
/**
* ..
* #ORM\HasLifecycleCallbacks
*/
class User
{
/**
* #ORM\PrePersist()
*/
public function setInitialFoo()
{
//Setting initial $foo value
}
}
But setting a relation value is not carried out by setting an integer id, rather it's carried out by adding an instance of Foo. And this can be done inside an event listener better than the entity's LifecycleCallback events (Because you'll have to call Foo entity's repository).
First, Register the event in your bundle services.yml file:
services:
user.listener:
class: Tsk\TestBundle\EventListener\FooSetter
tags:
- { name: doctrine.event_listener, event: prePersist }
And the FooSetter class:
namespace Tsk\TestBundle\EventListener\FooSetter;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Tsk\TestBundle\Entity\User;
class FooSetter
{
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof User) {
$foo = $entityManager->getRepository("TskTestBundle:Foo")->find(1);
$entity->addFoo($foo);
}
}
}
I would stay well away from listeners in this simple example, and also passing the EntityManager into an entity.
A much cleaner approach is to pass the entity you require into the new entity:
class User
{
public function __construct(YourEntity $entity)
{
parent::__construct();
$this->setFoo($entity);
}
Then elsewhere when you create a new entity, you will need to find and pass the correct entity in:
$foo = [find from entity manager]
new User($foo);
--Extra--
If you wanted to go further then the creation of the entity could be in a service:
$user = $this->get('UserCreation')->newUser();
which could be:
function newUser()
{
$foo = [find from entity manager]
new User($foo);
}
This would be my preferred way
You can't just pass the id of the relationship with 'Foo'. You need to retrieve the Foo entity first, and then set the foo property. For this to work, you will need an instance of the Doctrine Entity Manager. But then, you make your entity rely on the EntityManager, this is something you don't want.
Example:
// .../User.php
public function __construct(EntityManager $em) {
$this->em = $em;
$this->foo = $this->em->getRepository('Foo')->find(1);
}

Resources