Intent Bundle¶
Intent bundle integrates the Intent component into Symfony applications by providing a service for storing objects which have to be preserved between two stateless requests.
Installation¶
You can install the bundle using Composer:
composer require runopencode/intent-bundle
Depending on the driver which you intend to use, doctrine/dbal is required
for storing intents within a database table, while doctrine/orm is required
if you want the storage table to be generated along with the rest of your
schema. Both are, most commonly, already a part of your project.
And then enable the bundle in your Symfony application:
1<?php
2
3// config/bundles.php
4
5return [
6 // ...
7 RunOpenCode\Bundle\IntentBundle\IntentBundle::class => ['all' => true],
8];
Configuration¶
The bundle allows you to choose the driver which is used for storing intents, and to configure it. By default, intents are stored within a database table by using the default Doctrine Dbal connection.
1<?php
2// config/packages/runopencode_intent.php
3use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
4
5return static function (ContainerConfigurator $container): void {
6 $container->extension('runopencode_intent', [
7 'driver' => 'dbal',
8 'dbal' => [
9 'connection' => 'Doctrine\DBAL\Connection',
10 'table_name' => 'runopencode_intent',
11 ],
12 ]);
13};
1# config/packages/runopencode_intent.yaml
2runopencode_intent:
3 driver: dbal
4 dbal:
5 connection: 'Doctrine\DBAL\Connection'
6 table_name: runopencode_intent
Configuration options¶
driver(enum, default:dbal): driver used to persist intents, eitherdbalorcache. Only the configuration of the chosen driver is used, while the other one is ignored.dbal.connection(string, default:Doctrine\DBAL\Connection): service id of the connection within which intents are stored. By default, the default connection is used. If you have more than one connection, provide the service id of the connection of your choice, in exampledoctrine.dbal.reporting_connection.dbal.table_name(string, default:runopencode_intent): name of the table within which intents are stored.cache.pool(string, default:cache.app): service id of the PSR-6 cache pool within which intents are stored.
Storing intents within a cache pool¶
If you would rather store intents within Redis, Memcached, or any other cache
implementation, configure the cache driver. It is advisable to use a
dedicated cache pool for that purpose, so that clearing an application cache
does not remove pending intents as well.
1<?php
2// config/packages/runopencode_intent.php
3use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
4
5return static function (ContainerConfigurator $container): void {
6 $container->extension('framework', [
7 'cache' => [
8 'pools' => [
9 'app.cache_pool.intent' => [
10 'adapter' => 'cache.adapter.redis',
11 ],
12 ],
13 ],
14 ]);
15
16 $container->extension('runopencode_intent', [
17 'driver' => 'cache',
18 'cache' => [
19 'pool' => 'app.cache_pool.intent',
20 ],
21 ]);
22};
1# config/packages/cache.yaml
2framework:
3 cache:
4 pools:
5 app.cache_pool.intent:
6 adapter: cache.adapter.redis
7
8# config/packages/runopencode_intent.yaml
9runopencode_intent:
10 driver: cache
11 cache:
12 pool: app.cache_pool.intent
Do note that a cache pool is a cache, and that it is allowed to evict an item
before it expires. If losing an intent is not acceptable for your use case, use
the dbal driver instead.
Database schema¶
Identifier of an intent is stored as ULID. Bundle registers that Doctrine type
for you, so there is no need to add it to the doctrine.dbal.types
configuration on your own.
When the dbal driver is used, the storage registers itself as a listener for
the Doctrine ORM postGenerateSchema event. Table within which intents are
stored is, therefore, a part of your schema and it will be generated for you:
bin/console doctrine:schema:update --dump-sql
If you are using Doctrine migrations, table will be a part of a generated migration as well:
bin/console doctrine:migrations:diff
Note that Doctrine ORM is required for this convenience only. If your project uses Doctrine Dbal without ORM, everything else works as expected, but you will have to create the table on your own.
Usage¶
Once the bundle is installed and configured, the storage is automatically available for dependency injection. Inject it using the interface:
1<?php
2
3declare(strict_types=1);
4
5namespace App\Security;
6
7use App\Security\Intent\ResetPassword;
8use RunOpenCode\Component\Intent\Contract\IntentStorageInterface;
9use RunOpenCode\Component\Intent\Exception\NotExistsException;
10use Symfony\Component\Uid\Ulid;
11
12final readonly class PasswordResetService
13{
14 public function __construct(private IntentStorageInterface $storage)
15 {
16 // noop.
17 }
18
19 public function request(User $user): string
20 {
21 // Intent is valid for one hour only.
22 $identifier = $this->storage->store(new ResetPassword($user->getId()), 3600);
23
24 return (string)$identifier;
25 }
26
27 public function complete(string $token, string $password): void
28 {
29 try {
30 /** @var ResetPassword $intent */
31 $intent = $this->storage->fetch(Ulid::fromString($token));
32 } catch (NotExistsException) {
33 // Link is invalid, expired, or it has been used already.
34 // ...
35 }
36
37 // ...
38 }
39}
Read more about how to store and fetch intents in the Intent component documentation.
Removing expired intents¶
Expired intents are removed from the storage when they are fetched, which is not enough if a link is never clicked. For that purpose, a console command is provided:
bin/console runopencode:intent:maintenance
Do note that this command does nothing when the cache driver is used, since
cache pools evict expired items on their own.