Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Activating Timestampable

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ok, let's add Timestampable! First, we need to activate it, which again, is described way down on the bundle's docs. Open config/packages/stof_doctrine_extensions.yaml, and add timestampable: true:

... lines 1 - 2
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
sluggable: true
timestampable: true

Second, your entity needs some annotations. For this, go back to the library's docs. Easy enough: we just need @Gedmo\Timestampable.

Back in our project, open Article and scroll down to find the new fields. Above createdAt, add @Timestampable() with on="create":

... lines 1 - 10
class Article
{
... lines 13 - 55
/**
* @ORM\Column(type="datetime")
* @Gedmo\Timestampable(on="create")
*/
private $createdAt;
... lines 61 - 190
}

Copy that, paste above updatedAt, and use on="update":

... lines 1 - 10
class Article
{
... lines 13 - 61
/**
* @ORM\Column(type="datetime")
* @Gedmo\Timestampable(on="update")
*/
private $updatedAt;
... lines 67 - 190
}

That should be it! Find your terminal, and reload the fixtures!

php bin/console doctrine:fixtures:load

No errors... but, let's make sure it's actually working. Run:

php bin/console doctrine:query:sql 'SELECT * FROM article'

Yes! They are set! And each time we update, the updated_at will change.

The TimestampableEntity Trait

I love Timestampable. Heck, I put it everywhere. And, fortunately, there is a shortcut! Yea, we did way too much work.

Check it out: completely delete the createdAt and updatedAt fields that we so-carefully added. And, remove the getter and setter methods at the bottom too:

... lines 1 - 10
class Article
{
... lines 13 - 55
/**
* @ORM\Column(type="datetime")
* @Gedmo\Timestampable(on="create")
*/
private $createdAt;
/**
* @ORM\Column(type="datetime")
* @Gedmo\Timestampable(on="update")
*/
private $updatedAt;
... lines 67 - 167
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(?\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(?\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
}

But now, all the way on top, add use TimestampableEntity:

... lines 1 - 6
use Gedmo\Timestampable\Traits\TimestampableEntity;
... lines 8 - 11
class Article
{
use TimestampableEntity;
... lines 15 - 157
}

Yea! Hold Command or Ctrl and click to see that. Awesome: this contains the exact same code that we had before! If you want Timestampable, just use this trait, generate a migration and... done!

And, talking about migrations, there could be some slight column differences between these columns and the original ones we created. Let's check that. Run:

php bin/console make:migration

No database changes were detected

Cool! The fields in the trait are identical to what we had before. That means that we can already test things with:

php bin/console doctrine:fixtures:load

Thank you TimestampableEntity!

Up Next: Relations!

Ok guys! I hope you are loving Doctrine! We just got a lot of functionality fast. We have magic - like Timestampable & Sluggable - rich data fixtures, and a rocking migration system.

One thing that we have not talked about yet is production config. And... that's because it's already setup. The Doctrine recipe came with its own config/packages/prod/doctrine.yaml config file, which makes sure that anything that can be cached easily, is cached:

doctrine:
orm:
metadata_cache_driver:
type: service
id: doctrine.system_cache_provider
query_cache_driver:
type: service
id: doctrine.system_cache_provider
result_cache_driver:
type: service
id: doctrine.result_cache_provider
services:
doctrine.result_cache_provider:
class: Symfony\Component\Cache\DoctrineProvider
public: false
arguments:
- '@doctrine.result_cache_pool'
doctrine.system_cache_provider:
class: Symfony\Component\Cache\DoctrineProvider
public: false
arguments:
- '@doctrine.system_cache_pool'
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

This means you get nice performance, out-of-the-box.

The other huge topic that we have not talked about yet is Doctrine relations. But, we should totally talk about those - they're awesome! So let's do that in our next tutorial, with foreign keys, join queries and high-fives so that we can create a really rich database.

Alright guys, seeya next time.

Leave a comment!

24
Login or Register to join the conversation

Does stof_doctrine_extensions Bundle need Doctrine extension Bundle ?

2 Reply

Hey Ahmed,

Could you clarify what exactly "Doctrine extension Bundle"?

Yes, StofDoctrineExtensionsBundle requires "gedmo/doctrine-extensions" package as the bundle is just a wrapper for that library that has integration with Symfony if you mean this.

Cheers!

Reply

Thanks! Looking forward to learn even more and high five an ice cream!

1 Reply
Default user avatar
Default user avatar Quntun El Andaluz Trtinoh | posted 1 year ago

Hi guys, how does it work if I want to create a timestampable deleted_at ?
does the @Gedmo\Timestampable(on="delete") is supported too ?
I would like to have that information to keep a record when user "soft deleted" his account to keep a record for the 3 years storaged required as per law.
Many thanks in advance for your help.
https://sharemycode.fr/ni4

Reply

Hey Quntun El Andaluz Trtinoh

That IS a good question and BTW doctrine extensions has a mechanism for it. Try to use SoftDeleteable behavior, you can find docs here and I think a good example here

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 1 year ago

Can I show seconds from January 1st 1970 in Twig, like in PHP time()? I cant find it on the documentation.

Reply

Hey Farry7,

Sure, you can... Well, you need a datetime object first :) With the "date()" Twig function you can create a datetime object right in the template, and since it's a datetime object - print the timestamp as "{{ date().timestamp }}"

Another way would be to format a string with "|date" filter, e.g. "{{ 'now'|date('U') }}" will format the current time to the UNIX timestamp.

If both you don't like, pass the time() value from the controller to the template, or even create your own Twig filter for that PHP time() call :)

Cheers!

1 Reply

Hi there ! It's me a gain, as promised! :D
Now, I am not quite sure what to google for, that's why I thought I'd ask here. I want my app to do the following thing:
A user defines on a page a certain "object". Let's say there's a form and the form can take a variable amount of inputs. Now those inputs are here to define the database column names. Let's say my user wants to create a "Delivery address"-object/table (whatever you want to call it) and fills out the form with the fields "name", "address", "phone number", etc. ...
Aussming the user submitted the data correctly- what would be the best practice to generate dynamic database tables from here? Every user can have multiple, different tables, since the objects he creates can be different each time.

I have no clue how to do some research on this topic? Where do I begin?
To clarify the situation: Currently I am stuck at a point, where I am generating entities and I noticed, that some of my entities can have "sub"-entities, which I don't know how they look like yet.

Thank you very much!

EDIT: From there the generated objects need to have a relationship to mentioned entity from earlier. I can't wrap my head around this, since we're talking about things, that don't exist yet.
EDIT2: Is the approach of "let the user create his object/table, then somehow make a migration automatically" a thing? If so, how?

Reply

Hey denizgelion

That's a very good question and to be honest I'm not an expert when talking about dynamic tables in the Database but I googled around and found this SO question that may help you out or at least give you some ideas https://stackoverflow.com/q...

If you'll stick with a relational database (as it's MySql) I think you can build that system up on top of Doctrine DBAL

Cheers!

Reply
Satheesh S. Avatar
Satheesh S. Avatar Satheesh S. | posted 2 years ago

hi,
i am using symfony 5. i got this error while running php bin/console doctrine:fixtures:load

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'created_at' in 'field list'

please help!

Reply

Hey Satheesh,

Basically, if you're running this project locally, I suppose you just need to run "bin/console doctrine:schema:update --force" - this will update your schema to match entities mapping. In case you wonder what queries will be executed - run "bin/console doctrine:schema:update --dump-sql" instead to see the actual queries, and then run with "--force" flag to make those changes.

But the reason behind this error might be different, it's difficult to say something more without understanding of what exactly you're doing. Did you download the course code and started from start/ folder? Did you follow the steps in start/README.md file? Probably you missed some steps from README, or didn't run migrations, etc.

Cheers!

Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 2 years ago

I use SF 5.0.4 with the latest package/recipe updates and have a problem using timestampable with SF Forms.

I use the timestampable trait inside my entity:
use TimestampableEntity;

And inside SF Forms, I bind that form to my entity via:
'data_class' => Article::class,

But when I submit the form, I get an error that "data.createdAt and data.updatedAt should not be null".
Afte Googling the problem, the reason seems to be the new feature of SF since 4.3 "automatic validation" (Source).
I found two solutions:

1.) A Workaround:
Setting the value manually: Source

2.) A "real" fix via the new Annotation (Source):
/**
* @Assert\DisableAutoMapping()
* ...
*/
protected $createdAt;
...

It works, when I insert this Annotation inside the Trait.
But I shouldn't edit the trait itself, because its a vendor file, and if an update of timestampable is released, my code change will be overwritten.

So my question:
How do I tell SF Form that createdAt and updatedAt from the timestampable trait should be "ignored"/"will be automatically filled" in the validation process?
It seems to be my best option to copy the "original" trait to /src/Trait/TimestampableEntity.php with my changes and use my own trait instead?
Or what is the most professional solution?

Sidenote:
I have the same error for "sluggable should not be null", but the fix is easy here, I just use the annotation.

Reply

Hey Mike P.!

Boooooo. I mean, boooo because you've identified a really annoying situation where the auto-mapping stuff works really poorly! I'll say that IU like the auto-mapping feature a lot - we talk about it here https://symfonycasts.com/sc... for people who are not familiar - and I also really like this use of traits. However, I can't think of a way for them to work cleanly together.

About your solutions, I don't see any real issue with (1) - initializing the values in the constructor. That shouldn't be necessary, so it's kind of annoying, but it's really a solid way to do it. For solution (2), it's in some ways, hackier than (1) - you're re-declaring the property just to disable auto-mapping. That's a bummer.

Unfortunately, the library author *also* can't fix things by adding the @Assert\DisableAutoMapping() to *their* class as this would cause an error in any projects that aren't using Symfony's validator component.

So...

> It seems to be my best option to copy the "original" trait to /src/Trait/TimestampableEntity.php with my changes and use my own trait instead?
Or what is the most professional solution?

This is what I would do :). You're ultimately using the trait because it's convenient. If you need to do a workaround in every entity, it's not convenient anymore. Fortunately, I don't see any big problem with this: this code is dead-simple and will never change. The trait is provided by the library as a convenience. There may be some way that Symfony allows this to be solved in the future in a more "professional" way - but I don't think there is right now.

Cheers!

1 Reply
David B. Avatar
David B. Avatar David B. | posted 3 years ago

Hi Victor! I have thoughts about updatedAt field default value: isn't it need to be NULL from start, before making any actions more than creation?

Reply

Hey Bagar!

Good question! Well, it depends on your strategy, you can do whatever is better for you. Though, IIRC Timestampable strategy on StofDoctrineExtensionsBundle also update updatedAt on creating, most probably for technical reasons I think. It will allow you to get correct results on ordering your DB entries. If some of them would be NULL - you would get weird ordering results, you can try and see how it works in MySQL DB. Because NULL fields in SQL databases are "special", you need to use a different where conditions for them, etc. So, sometimes avoid NULL values is a good idea. But generally, it's not too much important I think, you just need to understand how it works in your project to avoid some WTF moments later.

Does it makes sense to you?

Cheers!

Reply
David B. Avatar

Yes Victor, of course, thank you!
Unfortunately, there is another problem I've faced recently:
https://github.com/stof/Sto...
hope that @stof will back ASAP

Reply

Hey Bagar,

Ah, I see... well, since it's just a deprecation - it should still work, most probably until Symfony 5.0 where all the deprecations will be removed, but I see your point. There's another bundle called KnpLabs/DoctrineBehaviors that also has timestampable behavior, but I haven't used it for a while, not sure how active it is, but just in case you're wondering. But if you need the only one timestampable behaviour - it's easily to implement it yourself in case you want to practice even listeners, but advantage of using libraries is that they well tested and solve some edge cases you may miss in your implementation. But it's an open source, so everybody can pick that task and try to fix the problem, then suggest the fix as a PR. If you have some time - you're also welcome to try to fix those deprecations ;)

Cheers!

1 Reply
Ozornick Avatar
Ozornick Avatar Ozornick | posted 3 years ago

Check it out: completely delete the createdAt and updatedAt fields that we so-carefully added. And, remove the getter and setter methods at the bottom too:
And then I cried...
Thanks for the course

Reply

Hey Ozornick,

Oh, we're sorry for killing your own lovely code :) But less code - less bugs ;)

Cheers!

1 Reply

Being a sf + doctrine user for years I could not believe I would learn anything new - I just went for the badge, but you proved me wrong. I still learned a thing or two.
Great job guys!

Reply

That’s the greatest compliment :). Cheers!

1 Reply

Hey Edin,

Glad you discovered something new ;) And thank you for your feedback!

Cheers!

Reply

I have used doctrine for years now and I keep learning new things that make it so much better. Thanks.

May I ask, when will the next course come out? Should I review the Symfony 3 Forms: Build, Render & Conquer! for the latest info on forms or is there something better to review?

Reply

Hey Skylar

Right now we are delivering the ReactJS tutorial and after that, we will release Symfony4 security tutorial. So, if you are looking for something specific to Symfony forms, then watching our Symfony3 tutorials about that topic would be a good idea, and anyways, it didn't change dramatically :)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0" // v4.0.14
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "fzaninotto/faker": "^1.7", // v1.7.1
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/maker-bundle": "^1.0", // v1.4.0
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.4
    }
}