Usage¶
Regardless of the storage which you have chosen, you will always work with
RunOpenCode\Component\Intent\Contract\IntentStorageInterface. It exposes
four methods only, which are covered in detail within this document.
Storing an intent¶
Any serializable object may be stored as an intent. There is no interface to implement and no attribute to add, an intent is just a plain object which describes what should be done once it is fetched:
1<?php
2
3declare(strict_types=1);
4
5namespace App\Security\Intent;
6
7final readonly class ResetPassword
8{
9 public function __construct(
10 public int $userId,
11 public \DateTimeImmutable $requestedAt,
12 ) {
13 // noop.
14 }
15}
Once stored, an identifier is returned, which is an instance of
Symfony\Component\Uid\Ulid:
1<?php
2
3$identifier = $storage->store(new ResetPassword($userId, new \DateTimeImmutable('now')));
Time to live¶
By default, an intent is stored for one day (86400 seconds). You may provide a different time to live, expressed in seconds, as a second argument:
1<?php
2
3// Intent is available for one hour only.
4$identifier = $storage->store($intent, 3600);
Once an intent expires, it is no longer available and it will be removed from the storage.
Deferred availability¶
You may, optionally, store an intent which becomes available at some moment in the future by providing a third argument:
1<?php
2
3// Intent becomes available tomorrow and it is available for one day.
4$identifier = $storage->store($intent, 86400, new \DateTimeImmutable('+1 day'));
Note that time to live is relative to that moment, and not to the moment when the intent has been stored. In example given above, the intent is available between tomorrow and the day after tomorrow.
An intent which is not available yet behaves exactly as an intent which does not exist. However, it is not removed from the storage, so it will become available once its time comes.
Fetching an intent¶
An intent is fetched by using its identifier:
1<?php
2
3/** @var ResetPassword $intent */
4$intent = $storage->fetch($identifier);
You will get the very same object which you have stored, so you may safely type hint against your own classes.
If an intent does not exist, if it has expired, if it is not available yet, or
if it has been fetched already, NotExistsException is thrown. From the
perspective of the code which fetches it, all these cases are the same and
should be handled in the same manner:
1<?php
2
3use RunOpenCode\Component\Intent\Exception\NotExistsException;
4
5try {
6 $intent = $storage->fetch($identifier);
7} catch (NotExistsException) {
8 // Link is invalid, expired, or it has been used already.
9}
Preserving an intent¶
An intent is invalidated as soon as it is fetched, which makes one time links the default behaviour. You may, however, fetch an intent without invalidating it, which is useful when you have to render a form before the use case is actually completed:
1<?php
2
3// Render the form, intent is still available.
4$intent = $storage->fetch($identifier, false);
5
6// ...
7
8// Form is submitted, intent is consumed now.
9$intent = $storage->fetch($identifier);
Invalidating an intent¶
An intent may be invalidated explicitly, in example, when a user cancels the process which has been initiated:
1<?php
2
3$storage->invalidate($identifier);
Method does not throw an exception if an intent with the given identifier does not exist.
Removing expired intents¶
Expired intents are removed from the storage when they are fetched, which is not enough if a link is never clicked. Therefore, storages have to be maintained from time to time:
1<?php
2
3$storage->maintenance();
How often you should invoke this method depends on how many intents you store and for how long.
Do note that storages which are able to remove expired intents on their own do nothing here. Such is the storage which uses a PSR-6 cache pool, since cache pools evict expired items themselves.
Console command¶
Since maintenance has to be executed periodically, a console command is provided
for that purpose. It requires symfony/console only, so you may use it within
any console application, regardless of the framework which you are using, or of
whether you are using one at all:
1<?php
2
3declare(strict_types=1);
4
5use RunOpenCode\Component\Intent\Command\ClearExpiredIntentsCommand;
6use Symfony\Component\Console\Application;
7
8$application = new Application();
9
10$application->addCommand(new ClearExpiredIntentsCommand($storage));
11
12$application->run();
Command name and description are declared by using the #[AsCommand]
attribute, so there is nothing else for you to configure. Once registered, it is
available as:
bin/console runopencode:intent:maintenance
If you are using Symfony framework, command is registered for you, see the Intent Bundle documentation.